Pārlūkot izejas kodu

Initialization of connector platform

Lukas Cerny 5 gadi atpakaļ
vecāks
revīzija
28c584e034
88 mainītis faili ar 4462 papildinājumiem un 0 dzēšanām
  1. 22 0
      .gitignore
  2. 85 0
      build.gradle
  3. 65 0
      config/test.yaml
  4. 0 0
      connector-app/build.gradle
  5. 78 0
      connector-app/src/main/java/io/connector/app/AppConfig.java
  6. 76 0
      connector-app/src/main/java/io/connector/app/Application.java
  7. 87 0
      connector-app/src/main/java/io/connector/app/Parameters.java
  8. 2 0
      connector-app/src/main/resources/project.properties
  9. 0 0
      connector-core/build.gradle
  10. 321 0
      connector-core/src/main/java/io/connector/core/AbstractGateway.java
  11. 36 0
      connector-core/src/main/java/io/connector/core/AddressPath.java
  12. 75 0
      connector-core/src/main/java/io/connector/core/DataCollection.java
  13. 24 0
      connector-core/src/main/java/io/connector/core/Handler.java
  14. 22 0
      connector-core/src/main/java/io/connector/core/MessageHeader.java
  15. 66 0
      connector-core/src/main/java/io/connector/core/ModuleDeployer.java
  16. 29 0
      connector-core/src/main/java/io/connector/core/ModuleDescriptor.java
  17. 190 0
      connector-core/src/main/java/io/connector/core/VertxServer.java
  18. 51 0
      connector-core/src/main/java/io/connector/core/config/ConfigurationService.java
  19. 45 0
      connector-core/src/main/java/io/connector/core/config/DefaultConfig.java
  20. 25 0
      connector-core/src/main/java/io/connector/core/config/HostConfig.java
  21. 183 0
      connector-core/src/main/java/io/connector/core/config/PropertyConfig.java
  22. 32 0
      connector-core/src/main/java/io/connector/core/config/SchedulerConfig.java
  23. 51 0
      connector-core/src/main/java/io/connector/core/config/database/DatabaseBuilder.java
  24. 53 0
      connector-core/src/main/java/io/connector/core/config/database/DatabaseBuilderImpl.java
  25. 22 0
      connector-core/src/main/java/io/connector/core/config/database/DatabaseConfigurationService.java
  26. 66 0
      connector-core/src/main/java/io/connector/core/config/database/DatabaseConfigurationServiceImpl.java
  27. 29 0
      connector-core/src/main/java/io/connector/core/config/file/FileBuilder.java
  28. 40 0
      connector-core/src/main/java/io/connector/core/config/file/FileBuilderImpl.java
  29. 24 0
      connector-core/src/main/java/io/connector/core/config/file/FileConfigurationService.java
  30. 194 0
      connector-core/src/main/java/io/connector/core/config/file/FileConfigurationServiceImpl.java
  31. 6 0
      connector-core/src/main/java/io/connector/core/json/DeserializeFormatter.java
  32. 41 0
      connector-core/src/main/java/io/connector/core/json/JsonAttributeFormatter.java
  33. 23 0
      connector-core/src/main/java/io/connector/core/json/JsonDeserializer.java
  34. 22 0
      connector-core/src/main/java/io/connector/core/json/JsonSerializer.java
  35. 6 0
      connector-core/src/main/java/io/connector/core/json/SerializeFormatter.java
  36. 59 0
      connector-core/src/main/java/io/connector/core/module/AbstractModule.java
  37. 59 0
      connector-core/src/main/java/io/connector/core/module/ModuleInfo.java
  38. 41 0
      connector-core/src/main/java/io/connector/core/module/ModuleLoader.java
  39. 8 0
      connector-core/src/main/java/io/connector/core/module/ModuleProvider.java
  40. 11 0
      connector-core/src/main/java/io/connector/core/module/ModuleType.java
  41. 0 0
      connector-model/build.gradle
  42. 22 0
      connector-model/src/main/java/io/connector/model/afarcloud/AFCModel.java
  43. 6 0
      connector-model/src/main/java/io/connector/model/afarcloud/Observation.java
  44. 14 0
      connector-model/src/main/java/io/connector/model/ima/IMAModel.java
  45. 113 0
      connector-model/src/main/java/io/connector/model/senslog1/Observation.java
  46. 36 0
      connector-model/src/main/java/io/connector/model/senslog1/Phenomenon.java
  47. 65 0
      connector-model/src/main/java/io/connector/model/senslog1/Position.java
  48. 90 0
      connector-model/src/main/java/io/connector/model/senslog1/SensorInfo.java
  49. 120 0
      connector-model/src/main/java/io/connector/model/senslog1/SensorObservation.java
  50. 175 0
      connector-model/src/main/java/io/connector/model/senslog1/UnitData.java
  51. 84 0
      connector-model/src/main/java/io/connector/model/senslog1/UnitInfo.java
  52. 0 0
      connector-module-afarcloud/build.gradle
  53. 25 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCClient.java
  54. 10 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCConfig.java
  55. 31 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCModule.java
  56. 17 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCModuleProvider.java
  57. 18 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/UnitCollection.java
  58. 24 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/gateway/AFCGateway.java
  59. 101 0
      connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/gateway/SensLog1Gateway.java
  60. 1 0
      connector-module-afarcloud/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider
  61. 0 0
      connector-module-ima/build.gradle
  62. 21 0
      connector-module-ima/src/main/java/io/connector/module/ima/AFCConverter.java
  63. 18 0
      connector-module-ima/src/main/java/io/connector/module/ima/IMAClient.java
  64. 10 0
      connector-module-ima/src/main/java/io/connector/module/ima/IMAConfig.java
  65. 26 0
      connector-module-ima/src/main/java/io/connector/module/ima/IMAModule.java
  66. 18 0
      connector-module-ima/src/main/java/io/connector/module/ima/IMAModuleProvider.java
  67. 1 0
      connector-module-ima/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider
  68. 0 0
      connector-module-ogc-sensorthings/build.gradle
  69. 14 0
      connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsClient.java
  70. 10 0
      connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsConfig.java
  71. 31 0
      connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsModule.java
  72. 21 0
      connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsModuleProvider.java
  73. 50 0
      connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/gateway/SensorThingsGateway.java
  74. 1 0
      connector-module-ogc-sensorthings/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider
  75. 0 0
      connector-module-senslog1/build.gradle
  76. 386 0
      connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1Client.java
  77. 43 0
      connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1Config.java
  78. 50 0
      connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1Module.java
  79. 19 0
      connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1ModuleProvider.java
  80. 24 0
      connector-module-senslog1/src/main/java/io/connector/module/senslog1/gateway/AFCGateway.java
  81. 216 0
      connector-module-senslog1/src/main/java/io/connector/module/senslog1/gateway/SensLog1Gateway.java
  82. 1 0
      connector-module-senslog1/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider
  83. BIN
      gradle/wrapper/gradle-wrapper.jar
  84. 6 0
      gradle/wrapper/gradle-wrapper.properties
  85. 172 0
      gradlew
  86. 84 0
      gradlew.bat
  87. 9 0
      settings.gradle
  88. 10 0
      src/main/java/io/connector/Main.java

+ 22 - 0
.gitignore

@@ -107,3 +107,25 @@ $RECYCLE.BIN/
 # Windows shortcuts
 *.lnk
 
+# Log4J logging
+*.log
+
+### Gradle ###
+.gradle
+/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+# End of https://www.gitignore.io/api/java,gradle
+
+/bin/

+ 85 - 0
build.gradle

@@ -0,0 +1,85 @@
+plugins {
+    id 'application'
+}
+
+mainClassName = 'io.connector.Main'
+buildDir = 'bin'
+
+def moduleNames = subprojects.findAll {
+    it.name.startsWith("connector-module-")
+}
+
+dependencies {
+    compile project(":connector-app")
+}
+
+// setting for all projects
+allprojects {
+    sourceCompatibility = 1.8
+
+    // set that all projects are Java projects
+    apply plugin: 'java'
+
+    group 'io.connector'
+    version '2.0'
+
+    repositories {
+        mavenLocal()
+        mavenCentral()
+    }
+
+    dependencies {
+        compile group: 'cz.senslog', name: 'common', version: '1.0.0'
+    }
+
+    // create fat JAR
+    jar {
+        manifest {
+            attributes "Main-Class": "$mainClassName"
+        }
+
+        from {
+            configurations.runtimeClasspath.collect {
+                it.isDirectory() ? it : zipTree(it)
+            }
+        }
+    }
+}
+
+// settings for all modules
+subprojects {
+    buildDir = '../bin'
+}
+
+project("connector-app") {
+    dependencies {
+        compile project(":connector-core")
+        compile group: 'com.beust', name: 'jcommander', version: '1.78'
+    }
+}
+
+project(":connector-core") {
+
+    dependencies {
+        compile group: 'io.vertx', name: 'vertx-core', version: '3.9.1'
+        compile group: 'io.vertx', name: 'vertx-web', version: '3.9.1'
+    }
+}
+
+project(":connector-model") {
+
+    dependencies {
+        compile group: 'io.vertx', name: 'vertx-core', version: '3.9.1'
+    }
+}
+
+// settings for all 'connector-module-*'
+configure(moduleNames) {
+
+    dependencies {
+        compile project(":connector-core")
+        compile project(":connector-model")
+
+        compile group: 'io.vertx', name: 'vertx-core', version: '3.9.1'
+    }
+}

+ 65 - 0
config/test.yaml

@@ -0,0 +1,65 @@
+
+
+services:
+
+  senslog1:
+      api: &apiDomain
+        domain: "http://51.15.45.95:8080/senslog1"
+
+      name: "SensLog V1 Latvia"
+      provider: "io.connector.module.senslog1.SensLog1ModuleProvider"
+
+      user: "vilcini"
+      group: "vilcini"
+
+      sensorServiceHost:
+        <<: *apiDomain
+        path: "SensorService"
+
+      dataServiceHost:
+        <<: *apiDomain
+        path: "DataService"
+
+      feederServiceHost:
+        <<: *apiDomain
+        path: "FeederServlet"
+
+  OGC_SensorThings:
+
+      name: "OGC SensorThings V1"
+      provider: "io.connector.module.ogc.sensorthings.SensorThingsModuleProvider"
+
+      host:
+        <<: *apiDomain
+        path: "<path>"
+
+  IMA:
+      api: &apiDomain
+        domain: "https://iotlorawan.azurewebsites.net"
+
+      name: "IoT LoraWan"
+      provider: "io.connector.module.ima.IMAModuleProvider"
+
+      host:
+        <<: *apiDomain
+        path: "<path>"
+
+
+  AFC:
+      api: &apiDomain
+        domain: "https://rest.afarcloud.smartarch.cz/storage/rest/registry"
+
+      name: "AFarCloud"
+      provider: "io.connector.module.afarcloud.AFCModuleProvider"
+
+      host:
+        <<: *apiDomain
+        path: "<path>"
+
+scheduler:
+#  senslog1:
+#      period: 15
+#      consumer: "schedule-observations"
+#      config:
+#        allowedStations:
+#          345345: [1, 2, 3]

+ 0 - 0
connector-app/build.gradle


+ 78 - 0
connector-app/src/main/java/io/connector/app/AppConfig.java

@@ -0,0 +1,78 @@
+package io.connector.app;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * The class {@code AppConfig} represents basic configuration of
+ * the application. The configuration file is located in resources
+ * and the values are connected to the properties in parent pom.xml.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class AppConfig {
+
+    /** Name of the properties configuration file. */
+    private static final String PROPERTIES_FILE_NAME = "project.properties";
+
+    /** Attribute of loaded properties. */
+    private final Properties properties;
+
+    /**
+     * Static method to load the configuration file.
+     * @return new instance of {@code AppConfig}.
+     * @throws IOException throws if the file is not loaded successfully.
+     */
+    public static AppConfig load() throws IOException {
+        Properties properties = new Properties();
+        ClassLoader loader = AppConfig.class.getClassLoader();
+        InputStream stream = loader.getResourceAsStream(PROPERTIES_FILE_NAME);
+
+        properties.load(stream);
+
+        return new AppConfig(properties);
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static init method {@link AppConfig#load()}.
+     * @param properties - loaded properties from the file.
+     */
+    private AppConfig(Properties properties) {
+        this.properties = properties;
+    }
+
+    /**
+     * Returns name of the application defined in pom.xml
+     * @return name of the application or 'unknown'.
+     */
+    public String getName() {
+        return getProperty("app.name", "unknown");
+    }
+
+    /**
+     * Returns version of the application defined in pom.xml
+     * @return version of the application or empty string.
+     */
+    public String getVersion() {
+        return getProperty("app.version", "");
+    }
+
+    /**
+     * General method the get a property .
+     * @param propertyName - name of the property.
+     * @param defaultValue - default value if property does not exists.
+     * @return value of the property or default value.
+     */
+    private String getProperty(String propertyName, String defaultValue) {
+        String value = properties.getProperty(propertyName);
+
+        if (value == null) {
+            return defaultValue;
+        } else {
+            return value;
+        }
+    }
+}

+ 76 - 0
connector-app/src/main/java/io/connector/app/Application.java

@@ -0,0 +1,76 @@
+package io.connector.app;
+
+import cz.senslog.common.util.StringUtils;
+import io.connector.core.config.ConfigurationService;
+import io.connector.core.config.file.FileConfigurationService;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+
+import static io.connector.core.ModuleDeployer.deploy;
+import static io.connector.core.module.ModuleLoader.loadModules;
+
+public class Application extends Thread {
+
+    private static Logger logger = LogManager.getLogger(Application.class);
+
+    private final AppConfig appConfig;
+    private final Parameters params;
+
+
+    public static Thread init(String... args) throws IOException {
+        AppConfig appConfig = AppConfig.load();
+        Parameters parameters = Parameters.parse(appConfig, args);
+
+        if (parameters.isHelp()) {
+            return new Thread(parameters::printHelp);
+        }
+
+        return new Application(appConfig, parameters);
+    }
+
+    private Application(AppConfig appConfig, Parameters parameters) {
+        super("app");
+
+        this.appConfig = appConfig;
+        this.params = parameters;
+    }
+
+    @Override
+    public void run() {
+        logger.info("Starting the application {} of the version {}", appConfig.getName(), appConfig.getVersion());
+
+        ConfigurationService configService = null;
+
+        if (StringUtils.isNotBlank(params.getConfigFileName())) {
+            try {
+                FileConfigurationService service = ConfigurationService.newFileBuilder()
+                        .fileName(params.getConfigFileName()).build();
+
+                service.load();
+
+                configService = service;
+            } catch (IOException e) {
+                logger.catching(e); return;
+            }
+        } else {
+            logger.error("The application supports only by config file at this moment.");
+            System.exit(1);
+        }
+
+        DeploymentOptions options = new DeploymentOptions()
+                .setConfig(new JsonObject().put("http.server.port", params.getPort()));
+
+        Vertx.vertx().deployVerticle(deploy(loadModules(configService)), options, res -> {
+            if(res.succeeded()) {
+                logger.info("Deployment id is: {}", res.result());
+            } else {
+                logger.error("Deployment failed! The reason is '{}'", res.result());
+            }
+        });
+    }
+}

+ 87 - 0
connector-app/src/main/java/io/connector/app/Parameters.java

@@ -0,0 +1,87 @@
+package io.connector.app;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static cz.senslog.common.util.StringUtils.isNotBlank;
+import static java.lang.String.format;
+import static java.nio.file.Files.notExists;
+import static java.nio.file.Paths.get;
+
+/**
+ * The class {@code Parameters} represents input parameters from
+ * the applications. For parsing is used {@see JCommander} library.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+final class Parameters {
+
+    private static Logger logger = LogManager.getLogger(Parameters.class);
+
+    private JCommander jCommander;
+
+    /**
+     * Static method to parse input parameters.
+     * @param appConfig - main configuration of the application.
+     * @param args - array of parameters in format e.g. ["-cf", "fileName"].
+     * @return instance of {@code Parameters}.
+     * @throws IOException throws if is chosen "-cf" or "-config-file" parameter and the file does not exist.
+     */
+    public static Parameters parse(AppConfig appConfig, String... args) throws IOException {
+        logger.debug("Parsing input parameters {}", Arrays.toString(args));
+
+        Parameters parameters = new Parameters();
+        JCommander jCommander = JCommander.newBuilder()
+                .programName(appConfig.getName())
+                .addObject(parameters).build();
+        parameters.jCommander = jCommander;
+
+        jCommander.parse(args);
+
+        String configFileName = parameters.getConfigFileName();
+        logger.debug("Checking existence of configuration file {}", configFileName);
+        if (isNotBlank(configFileName) && notExists(get(configFileName))) {
+            throw new FileNotFoundException(format("Config file %s does not exist.", configFileName));
+        }
+
+        logger.info("Parsing input parameters {} were parsed successfully.", Arrays.toString(args));
+        return parameters;
+    }
+
+    @Parameter(names = {"-h", "-help"}, help = true)
+    private boolean help = false;
+
+    @Parameter(names = {"-cf", "-config-file"}, description = "Configuration file in .yaml format.")
+    private String configFileName;
+
+    @Parameter(names = {"-p", "-port"}, description = "Access port for HTTP server", required = true)
+    private int port;
+
+    /**
+     * Returns name of the configuration file.
+     * @return string name.
+     */
+    public String getConfigFileName() {
+        return configFileName;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public boolean isHelp() {
+        return help;
+    }
+
+    public void printHelp() {
+        jCommander.usage();
+    }
+}

+ 2 - 0
connector-app/src/main/resources/project.properties

@@ -0,0 +1,2 @@
+app.name=Connector
+app.version=2.0

+ 0 - 0
connector-core/build.gradle


+ 321 - 0
connector-core/src/main/java/io/connector/core/AbstractGateway.java

@@ -0,0 +1,321 @@
+package io.connector.core;
+
+import io.connector.core.module.ModuleType;
+import io.vertx.core.MultiMap;
+import io.vertx.core.eventbus.DeliveryOptions;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.eventbus.ReplyException;
+import io.vertx.ext.web.Router;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static io.connector.core.AddressPath.*;
+import static io.connector.core.AddressPath.Creator.create;
+import static io.connector.core.MessageHeader.*;
+import static io.vertx.core.eventbus.ReplyFailure.NO_HANDLERS;
+
+public abstract class AbstractGateway {
+
+    private final static Logger logger = LogManager.getLogger(AbstractGateway.class);
+
+    protected static class Message<T> {
+        private final io.vertx.core.eventbus.Message<T> message;
+        private Reply<Object> reply;
+
+        private Fail fail;
+
+        private Message(io.vertx.core.eventbus.Message<T> message) {
+            this.message=  message;
+        }
+
+        private Message(Throwable throwable) {
+            this.message = null;
+
+            if (throwable instanceof ReplyException) {
+                this.fail = new Fail(((ReplyException)throwable).failureCode(), throwable.getMessage());
+            } else {
+                this.fail = new Fail(400, throwable.getMessage());
+            }
+        }
+
+        public T body() {
+            return message.body();
+        }
+
+        public MultiMap headers() {
+            return message.headers();
+        }
+
+        public String address() {
+            return message.address();
+        }
+
+        public void fail(int code, String message) {
+            fail = new Fail(code, message);
+        }
+
+        public boolean success() {
+            return fail == null;
+        }
+
+        public boolean isFail() {
+            return !success();
+        }
+
+        public Fail cause() {
+            return fail;
+        }
+
+        public Reply<Object> reply(Object data) {
+            reply = new Reply<>(data);
+            return reply;
+        }
+    }
+
+    protected static class Fail {
+        private final int code;
+        private final String message;
+
+        Fail(int code, String message) {
+            this.code = code;
+            this.message = message;
+        }
+
+        public int getCode() {
+            return code;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+    }
+
+    protected static class Reply<T> {
+        private final T data;
+        private final DeliveryOptions options;
+
+        private Reply(T data) {
+            this.data = data;
+            this.options = new DeliveryOptions();
+        }
+
+        public DeliveryOptions options() {
+            return options;
+        }
+    }
+
+    private final EventBus eventBus;
+    private final Router router;
+    private final ModuleType moduleType;
+    protected final ModuleType gatewayType;
+    private final Map<String, String> registeredConsumers;
+    private final Map<String, String> schedulerMapping;
+    private final Map<String, Consumer<Message<Object>>> consumerHandlers;
+
+    protected AbstractGateway(EventBus eventBus, Router router, ModuleType moduleType, ModuleType gatewayType) {
+        this.eventBus = eventBus;
+        this.router = router;
+        this.moduleType = moduleType;
+        this.gatewayType = gatewayType;
+        this.registeredConsumers = new HashMap<>();
+        this.schedulerMapping = new HashMap<>();
+        this.consumerHandlers = new HashMap<>();
+    }
+
+    public String id() {
+        return gatewayType.name().toLowerCase();
+    }
+
+    public Router router() {
+        return router;
+    }
+
+    protected abstract void run();
+
+    public final void start() {
+        run();
+        registerSchedulerConsumers();
+    }
+
+    private String createAddress(String consumerName) {
+        if (!registeredConsumers.containsKey(consumerName)) {
+            throw logger.throwing(new RuntimeException(
+                    String.format("Consumer '%s' in module %s and gateway %s is not registered.",
+                            consumerName, moduleType.name().toLowerCase(), gatewayType.name().toLowerCase()
+                    )
+            ));
+        }
+        String encapsulation = registeredConsumers.get(consumerName);
+        return create(encapsulation, moduleType, gatewayType, consumerName);
+    }
+
+    private <T> void privateRequest(String address, Object body, DeliveryOptions options, Consumer<Message<T>> handler) {
+        // TODO request in the same gateway
+    }
+
+    private <T> void publicRequest(String address, Object body, DeliveryOptions options, Consumer<Message<T>> handler) {
+        // TODO request to another module
+    }
+
+
+    protected final <T> void request(String address, Object body, DeliveryOptions options, Consumer<Message<T>> handler) {
+        if (!registeredConsumers.containsKey(address)) {
+            handler.accept(new Message<>(new ReplyException(NO_HANDLERS, "no handler " + address))); return;
+        }
+
+        String encapsulation = registeredConsumers.get(address);
+        if (encapsulation.equals(PUBLIC_CONSUMER)) {
+            eventBus.<T>request(createAddress(address), body, options, reply -> {
+                Message<T> message;
+                if (reply.succeeded()) {
+                    message = new Message<>(reply.result());
+                } else {
+                    message = new Message<>(reply.cause());
+                }
+                handler.accept(message);
+            });
+        } else if (encapsulation.equals(PRIVATE_CONSUMER)) {
+
+
+        }
+    }
+
+    protected final <T> void publicConsumer(String address, Consumer<Message<T>> handler) {
+        registerConsumer(address, PUBLIC_CONSUMER, handler);
+    }
+
+    // TODO change handler to map of handlers without eventBus
+    protected final <T> void privateConsumer(String address, Consumer<Message<T>> handler) {
+        registerConsumer(address, PRIVATE_CONSUMER, handler);
+    }
+
+    private <T> void registerConsumer(String address, String encapsulation, Consumer<Message<T>> handler) {
+        if (registeredConsumers.containsKey(address)) {
+            logger.throwing(new RuntimeException(
+                    String.format("Address '%s' is already registered as '%s'.", address, registeredConsumers.get(address))
+            ));
+        }
+        registeredConsumers.put(address, encapsulation);
+        eventBus.<T>consumer(createAddress(address), message -> {
+            Message<T> msg = new Message<>(message);
+            handler.accept(msg);
+            if (msg.isFail()) {
+                Fail fail = msg.fail;
+                message.fail(fail.code, fail.message);
+            } else {
+                Reply<Object> reply = msg.reply;
+                if (reply != null) {
+                    reply.options
+                            .addHeader(MODULE_TYPE, moduleType.name())
+                            .addHeader(GATEWAY_TYPE, gatewayType.name())
+                            .addHeader(ADDRESS, address);
+                    message.reply(reply.data, reply.options);
+                } else {
+                    message.fail(204, "no content");
+                }
+            }
+        });
+    }
+
+    public interface SchedulerMapping {
+        SchedulerMapping addMapping(String from, String to);
+    }
+
+    protected final SchedulerMapping schedulerMapping() {
+        return new SchedulerMapping() {
+            @Override public SchedulerMapping addMapping(String from, String to) {
+                schedulerMapping.put(from, to);
+                return this;
+            }
+        };
+    }
+
+    private void registerSchedulerConsumers() {
+        final boolean isDefault = moduleType.equals(gatewayType);
+
+        for (Map.Entry<String, String> mappingEntry : schedulerMapping.entrySet()) {
+            if (isDefault) {
+                String privateConsumer = registeredConsumers.get(mappingEntry.getKey());
+                if (privateConsumer == null) {
+                    throw logger.throwing(new RuntimeException(
+                            String.format("Consumer '%s' in module %s and gateway %s is not registered.",
+                                    mappingEntry.getKey(),
+                                    moduleType.name().toLowerCase(),
+                                    gatewayType.name().toLowerCase()
+                            )
+                    ));
+                }
+
+                if (!privateConsumer.equalsIgnoreCase(PRIVATE_CONSUMER)) {
+                    logger.warn("Consumer '{}' in module {} and gateway {} is not private and could be dangerous.",
+                            mappingEntry.getKey(), moduleType.name().toLowerCase(), gatewayType.name().toLowerCase());
+                }
+            }
+
+            if (!registeredConsumers.containsKey(mappingEntry.getValue())) {
+                throw logger.throwing(new RuntimeException(
+                        String.format("Consumer '%s' in module %s and gateway %s is not registered.",
+                                mappingEntry.getValue(),
+                                moduleType.name().toLowerCase(),
+                                gatewayType.name().toLowerCase()
+                        )
+                ));
+            }
+        }
+
+        String baseAddr = isDefault ? SCHEDULER_PROVIDER : SCHEDULER_CONSUMER;
+        eventBus.consumer(create(baseAddr, gatewayType.name()), message -> {
+            if (isDefault) {
+                String proxyConsumerName = message.headers().get(ADDRESS);
+                // this is a provider and must request a local consumer
+                DeliveryOptions proxyOptions = new DeliveryOptions().setHeaders(message.headers());
+                // request proxy consumer (eq scheduler-observations)
+                eventBus.request(createAddress(proxyConsumerName), message.body(), proxyOptions, proxyReply -> {
+                    if (proxyReply.succeeded()) {
+                        io.vertx.core.eventbus.Message<Object> proxyMsg = proxyReply.result();
+                        String providerProxyName = proxyMsg.headers().get(ADDRESS); // hit: consumerName is the same as providerProxyName
+                        String providerName = schedulerMapping.get(providerProxyName);
+                        if (providerName != null) {
+                            DeliveryOptions providerOptions = new DeliveryOptions().setHeaders(proxyMsg.headers());
+                            // request provider consumer (eq observations)
+                            eventBus.request(createAddress(providerName), proxyMsg.body(), providerOptions, providerReply -> {
+                                if (providerReply.succeeded()) {
+                                    io.vertx.core.eventbus.Message<Object> providerMsg = providerReply.result();
+                                    DeliveryOptions replyOptions = new DeliveryOptions().setHeaders(providerMsg.headers());
+                                    message.reply(providerMsg.body(), replyOptions);
+                                }else {
+                                    message.fail(400, proxyReply.cause().getMessage());
+                                }
+                            });
+                        } else {
+                            message.fail(400, "No conversion endpoint for " + providerName);
+                        }
+                    } else {
+                        message.fail(400, proxyReply.cause().getMessage());
+                    }
+                });
+            } else {
+                String providerName = message.headers().get(ADDRESS);
+                String consumerName = schedulerMapping.get(providerName);
+                if (consumerName != null) {
+                    DeliveryOptions consumerOptions = new DeliveryOptions().setHeaders(message.headers());
+                    eventBus.request(createAddress(consumerName), message.body(), consumerOptions, consumerReply -> {
+                       if (consumerReply.succeeded()) {
+                           io.vertx.core.eventbus.Message<Object> consumerMsg = consumerReply.result();
+                           DeliveryOptions replyOptions = new DeliveryOptions().setHeaders(consumerMsg.headers());
+                           message.reply(consumerMsg.body(), replyOptions);
+                       } else {
+                           message.fail(400, consumerReply.cause().getMessage());
+                       }
+                    });
+                } else {
+                    message.fail(400, "No conversion endpoint for " + providerName);
+                }
+            }
+        });
+    }
+}

+ 36 - 0
connector-core/src/main/java/io/connector/core/AddressPath.java

@@ -0,0 +1,36 @@
+package io.connector.core;
+
+import io.connector.core.module.ModuleType;
+
+import static io.connector.core.AddressPath.Creator.create;
+
+public final class AddressPath {
+
+    public static final String SCHEDULER_CONSUMER = create("scheduler", "consumer");
+    public static final String SCHEDULER_PROVIDER = create("scheduler", "provider");
+
+    public static final String PRIVATE_CONSUMER = create("private");
+    public static final String PUBLIC_CONSUMER = create("public");
+
+    public static final String EVENT = create("event");
+    public static final String INFO = create("info");
+    public static final String HTTP_SERVER = create("api");
+
+
+    public static final class Creator {
+
+        private final static char DELIMITER = '/';
+
+        public static String create(String encapsulation, ModuleType moduleType, ModuleType gatewayType, String address) {
+            return create(encapsulation, moduleType.name(), gatewayType.name(), address);
+        }
+
+        public static String create(String ...parts) {
+            StringBuilder builder = new StringBuilder();
+            for (String part : parts) {
+                builder.append(part.charAt(0) == DELIMITER ? part : DELIMITER + part);
+            }
+            return builder.toString().toLowerCase();
+        }
+    }
+}

+ 75 - 0
connector-core/src/main/java/io/connector/core/DataCollection.java

@@ -0,0 +1,75 @@
+package io.connector.core;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.MessageCodec;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+public class DataCollection<T> {
+
+    private final List<T> list;
+
+    public DataCollection(List<T> list) {
+        this.list = new ArrayList<>(list);
+    }
+
+    @SafeVarargs
+    public static <T> DataCollection<T> of(T... dataList) {
+        return new DataCollection<>(Arrays.asList(dataList));
+    }
+
+    public static <T> DataCollection<T> create() {
+        return new DataCollection<>(new ArrayList<>());
+    }
+
+    public List<T> getList() {
+        return list;
+    }
+
+    public Iterator<Object> iterator() {
+        return (Iterator<Object>) list.iterator();
+    }
+
+    public void add(T data) {
+        this.list.add(data);
+    }
+
+    public int size() {
+        return list.size();
+    }
+
+    public static MessageCodec<DataCollection, DataCollection> createCodec() {
+        return new DataCollectionCodec();
+    }
+
+    private static class DataCollectionCodec implements MessageCodec<DataCollection, DataCollection> {
+
+        @Override
+        public void encodeToWire(Buffer buffer, DataCollection dataCollection) {
+
+        }
+
+        @Override
+        public DataCollection<?> decodeFromWire(int i, Buffer buffer) {
+            return null;
+        }
+
+        @Override
+        public DataCollection transform(DataCollection dataCollection) {
+            return dataCollection;
+        }
+
+        @Override
+        public String name() {
+            return getClass().getName();
+        }
+
+        @Override
+        public byte systemCodecID() {
+            return -1;
+        }
+    }
+}

+ 24 - 0
connector-core/src/main/java/io/connector/core/Handler.java

@@ -0,0 +1,24 @@
+package io.connector.core;
+
+import io.vertx.core.json.Json;
+import io.vertx.ext.web.RoutingContext;
+
+import java.util.function.Consumer;
+
+import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
+
+public final class Handler {
+
+    public static <T> Consumer<AbstractGateway.Message<T>> replyToHttpContext(RoutingContext httpContext) {
+        return reply -> {
+            if (reply.success()) {
+                httpContext.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+                httpContext.response().end(Json.encode(reply.body()));
+            } else {
+                AbstractGateway.Fail fail = reply.cause();
+                httpContext.fail(fail.getCode(), new RuntimeException(fail.getMessage()));
+            }
+        };
+    }
+}

+ 22 - 0
connector-core/src/main/java/io/connector/core/MessageHeader.java

@@ -0,0 +1,22 @@
+package io.connector.core;
+
+public final class MessageHeader {
+
+    public static final String RESOURCE = createName("resource");
+    public static final String MODULE_TYPE = createName("module_type");
+    public static final String GATEWAY_TYPE = createName("gateway_type");
+    public static final String ADDRESS = createName("address");
+
+    public static final class Resource {
+        public static final String SCHEDULER = "scheduler";
+        public static final String HTTP_SERVER = "http_server";
+    }
+
+    private static String createName(String name) {
+        return getPrefix() + name;
+    }
+
+    public static String getPrefix() {
+        return "__";
+    }
+}

+ 66 - 0
connector-core/src/main/java/io/connector/core/ModuleDeployer.java

@@ -0,0 +1,66 @@
+package io.connector.core;
+
+import io.connector.core.module.AbstractModule;
+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 ModuleDeployer extends AbstractVerticle {
+
+    private final static Logger logger = LogManager.getLogger(ModuleDeployer.class);
+
+    private final List<AbstractModule> modules;
+
+    public static ModuleDeployer deploy(List<AbstractModule> modules) {
+        return new ModuleDeployer(modules);
+    }
+
+    private ModuleDeployer(List<AbstractModule> modules) {
+        this.modules = modules;
+    }
+
+    @Override
+    public void start(Promise<Void> startPromise) {
+        List<Future> futureModules = new ArrayList<>(modules.size());
+
+        for (AbstractModule module : modules) {
+            DeploymentOptions options = new DeploymentOptions()
+                    .setWorker(true);
+            futureModules.add(deployHelper(vertx, options, module));
+        }
+
+        CompositeFuture.all(futureModules).onComplete(result -> {
+            if(result.succeeded()) {
+                DeploymentOptions serverOpt = new DeploymentOptions().setConfig(config());
+                vertx.deployVerticle(new VertxServer(modules), serverOpt, res -> {
+                    if (res.succeeded()) {
+                        startPromise.complete();
+                    } else {
+                        startPromise.fail(res.cause());
+                    }
+                });
+            } else {
+                startPromise.fail(result.cause());
+            }
+        });
+    }
+
+    private static <T extends AbstractVerticle> Future<Void> deployHelper(Vertx vertx, DeploymentOptions options, T module) {
+        logger.info("Deploying module: " + module.getClass().getName());
+        final Promise<Void> promise = Promise.promise();
+        vertx.deployVerticle(module, options, res -> {
+            if(res.failed()){
+                logger.error("Module {} was not deployed.", module.getClass().getName());
+                logger.catching(res.cause());
+                promise.fail(res.cause());
+            } else {
+                logger.info("Module {} was deployed successfully.", module.getClass().getName());
+                promise.complete();
+            }
+        });
+        return promise.future();
+    }
+}

+ 29 - 0
connector-core/src/main/java/io/connector/core/ModuleDescriptor.java

@@ -0,0 +1,29 @@
+package io.connector.core;
+
+import io.connector.core.config.DefaultConfig;
+import io.connector.core.config.SchedulerConfig;
+
+public class ModuleDescriptor {
+
+    private final String id;
+    private final DefaultConfig serviceConfig;
+    private final SchedulerConfig schedulerConfig;
+
+    public ModuleDescriptor(String id, DefaultConfig serviceConfig, SchedulerConfig schedulerConfig) {
+        this.id = id.toLowerCase();
+        this.serviceConfig = serviceConfig;
+        this.schedulerConfig = schedulerConfig;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public DefaultConfig getServiceConfig() {
+        return serviceConfig;
+    }
+
+    public SchedulerConfig getSchedulerConfig() {
+        return schedulerConfig;
+    }
+}

+ 190 - 0
connector-core/src/main/java/io/connector/core/VertxServer.java

@@ -0,0 +1,190 @@
+package io.connector.core;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.connector.core.config.SchedulerConfig;
+import io.connector.core.json.JsonAttributeFormatter;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleInfo;
+import io.vertx.core.*;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.DeliveryOptions;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.eventbus.Message;
+import io.vertx.core.eventbus.ReplyException;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.http.HttpServerResponse;
+import io.vertx.core.json.Json;
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.json.jackson.DatabindCodec;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.BodyHandler;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.OffsetDateTime;
+import java.util.*;
+
+import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
+import static cz.senslog.common.util.StringUtils.isBlank;
+import static io.connector.core.AddressPath.*;
+import static io.connector.core.AddressPath.Creator.*;
+import static io.connector.core.MessageHeader.*;
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+public class VertxServer extends AbstractVerticle {
+
+    private final static Logger logger = LogManager.getLogger(VertxServer.class);
+
+    private final List<AbstractModule> modules;
+
+    public VertxServer(List<AbstractModule> modules) {
+        this.modules = modules;
+    }
+
+    @Override
+    public void start(Promise<Void> startPromise) {
+        EventBus eventBus = vertx.eventBus();
+        Router router = Router.router(vertx);
+
+        eventBus.registerDefaultCodec(DataCollection.class, DataCollection.createCodec());
+        eventBus.registerDefaultCodec(ModuleInfo.class, ModuleInfo.createCodec());
+        ObjectMapper mapper = DatabindCodec.mapper();
+        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        mapper.registerModule(JsonAttributeFormatter.builder()
+                    .addSerializer(OffsetDateTime.class, val -> val.format(ISO_OFFSET_DATE_TIME))
+              .getModule()
+        );
+
+        for (AbstractModule module : modules) {
+            SchedulerConfig config = module.descriptor().getSchedulerConfig();
+            if (config != null && config.getPeriodSecond() > 0) {
+                vertx.setPeriodic(config.getPeriodMillisecond(), ctx -> {
+                    DeliveryOptions fetcherOpt = new DeliveryOptions()
+                            .addHeader(RESOURCE, MessageHeader.Resource.SCHEDULER)
+                            .addHeader(ADDRESS, config.getConsumer());
+
+                    // TODO config.getId() must the same as the enum ModuleType -> change the idea
+                    String address = create(SCHEDULER_PROVIDER, config.getId());
+                    eventBus.request(address, new JsonObject(), fetcherOpt, reply -> {
+                            if (reply.succeeded()) {
+                                Message<Object> result = reply.result();
+                                String source = result.headers().get(MODULE_TYPE); // TODO check if MODULE_TYPE of GATEWAY_TYPE
+                                DeliveryOptions pusherOpt = new DeliveryOptions();
+                                MultiMap headers = result.headers().addAll(fetcherOpt.getHeaders());
+                                for (Map.Entry<String, String> entryHeader : headers.entries()) {
+                                    if (entryHeader.getKey().startsWith(MessageHeader.getPrefix())) {
+                                        pusherOpt.addHeader(entryHeader.getKey(), entryHeader.getValue());
+                                    }
+                                }
+                                eventBus.publish(create(SCHEDULER_CONSUMER, source), result.body(), pusherOpt);
+                            } else {
+                                logger.catching(reply.cause());
+                            }
+                        });
+                });
+            }
+
+            router.mountSubRouter(create(HTTP_SERVER, module.id()), module.router());
+        }
+
+        router.route().failureHandler(ctx -> {
+            HttpServerResponse response = ctx.response();
+            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            JsonObject error = new JsonObject()
+                    .put("timestamp", System.nanoTime())
+                    .put("message", ctx.failure().getMessage())
+                    .put("path", ctx.request().path());
+            int code = ctx.statusCode() > 0 ? ctx.statusCode() : 400;
+            response.setStatusCode(code).end(error.encode());
+        });
+
+        router.route(create(HTTP_SERVER, "*")).handler(BodyHandler.create()).handler(ctx -> {
+
+            HttpServerRequest request = ctx.request();
+            HttpServerResponse response = ctx.response();
+
+            String moduleType = request.headers().get("Module-Type");
+            String gatewayType = request.headers().get("Gateway-Type");
+
+            if (isBlank(moduleType) || isBlank(gatewayType)) {
+                ctx.next(); return;
+            }
+
+            String path = ctx.normalisedPath().substring(4);
+            String address = create(PUBLIC_CONSUMER, moduleType, gatewayType, path);
+            Buffer bodyBuffer = ctx.getBody();
+            DeliveryOptions options = new DeliveryOptions().setHeaders(request.params())
+                    .addHeader(RESOURCE, MessageHeader.Resource.HTTP_SERVER);
+
+            eventBus.request(address, bodyBuffer, options, reply -> {
+                response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+                if (reply.succeeded()) {
+                    Message<Object> replyMessage = reply.result();
+
+                    for (Map.Entry<String, String> headerEntry : replyMessage.headers().entries()) {
+                        if (headerEntry.getKey().startsWith(MessageHeader.getPrefix())) { continue; }
+                        response.putHeader(headerEntry.getKey(), headerEntry.getValue());
+                    }
+
+                    String responseBody;
+                    if (replyMessage.body() instanceof DataCollection) {
+                        DataCollection<?> dataCollection = (DataCollection<?>) replyMessage.body();
+                        responseBody = Json.encode(dataCollection.getList());
+                    } else {
+                        responseBody = Json.encode(replyMessage.body());
+                    }
+
+                    response.end(responseBody);
+                } else {
+                    ctx.fail(reply.cause());
+                    Throwable throwable = reply.cause();
+                    int code = throwable instanceof ReplyException ? ((ReplyException)throwable).failureCode() : 400;
+                    ctx.fail(code, throwable);
+                }
+            });
+        });
+
+        router.get(EVENT).handler(ctx -> {
+
+        });
+
+        router.get(INFO).handler(ctx -> {
+            List<Future> futures = new ArrayList<>(modules.size());
+            for (AbstractModule module : modules) {
+                final Promise<ModuleInfo> promise = Promise.promise();
+                eventBus.<ModuleInfo>request(create(module.id(), INFO), new JsonObject(), reply -> {
+                    if (reply.succeeded()) {
+                        promise.complete(reply.result().body());
+                    } else {
+                        promise.fail(reply.cause());
+                    }
+                });
+                futures.add(promise.future());
+            }
+            CompositeFuture.all(futures).onComplete(reply -> {
+                HttpServerResponse response = ctx.response();
+                if(reply.succeeded()) {
+                    response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+                    response.end(Json.encode(reply.result().list()));
+                } else {
+                    response.end(reply.cause().getMessage());
+                }
+            });
+        });
+
+        int port = config().getInteger("http.server.port");
+        vertx.createHttpServer().requestHandler(router)
+                .listen(port, result -> {
+                    if (result.succeeded()) {
+                        logger.info("HTTP server running on port {}.", port);
+                        startPromise.complete();
+                    } else {
+                        logger.error("Could not start a HTTP server because of {}.", result.cause().getMessage());
+                        startPromise.fail(result.cause());
+                    }
+                });
+
+    }
+}

+ 51 - 0
connector-core/src/main/java/io/connector/core/config/ConfigurationService.java

@@ -0,0 +1,51 @@
+package io.connector.core.config;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.config.database.DatabaseBuilder;
+import io.connector.core.config.file.FileBuilder;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The interface {@code ConfigurationService} provides a generic service for configuration.
+ * Configuration can be gotten from a file, database or anything else.
+ *
+ * Provides two crucial functionalities:
+ *  - returns set of connector descriptors
+ *  - returns configuration for a class
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConfigurationService {
+
+    /**
+     * Creates a builder for a configuration from a file.
+     * @return new instance of {@link FileBuilder}.
+     */
+    static FileBuilder newFileBuilder() {
+        return FileBuilder.create();
+    }
+
+    /**
+     * Creates a builder for a configuration from a database.
+     * @return new instance of {@link DatabaseBuilder}.
+     */
+    static DatabaseBuilder newDatabaseBuilder() {
+        return DatabaseBuilder.create();
+    }
+
+    /**
+     * @return set of connector descriptors.
+     */
+    Set<ModuleDescriptor> getModuleDescriptors();
+
+    /**
+     * Returns a configuration depends on a provider id.
+     * @param id - identifier of the module.
+     * @return default configuration for an input provider id.
+     */
+    Optional<ModuleDescriptor> getModuleDescriptor(String id);
+}

+ 45 - 0
connector-core/src/main/java/io/connector/core/config/DefaultConfig.java

@@ -0,0 +1,45 @@
+package io.connector.core.config;
+
+/**
+ * The class {@code DefaultConfig} represents a major configuration class for all providers.
+ * Represents a root node of configuration for a class defines as a {@see #provider}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class DefaultConfig extends PropertyConfig {
+
+    /**
+     * Provider for which is gotten a configuration.
+     */
+    private final Class<?> provider;
+
+    /**
+     * Constructors sets root name (id) and provider class.
+     *
+     * @param id       - identifier of root node.
+     * @param provider - class provider.
+     */
+    public DefaultConfig(String id, Class<?> provider) {
+        super(id);
+        this.provider = provider;
+    }
+
+    public Class<?> getProviderClass() {
+        return provider;
+    }
+
+    @Override
+    public int hashCode() {
+        return provider.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DefaultConfig that = (DefaultConfig) o;
+        return hashCode() == that.hashCode();
+    }
+}

+ 25 - 0
connector-core/src/main/java/io/connector/core/config/HostConfig.java

@@ -0,0 +1,25 @@
+package io.connector.core.config;
+
+public class HostConfig {
+
+    private final String domain;
+    private final String path;
+
+    public HostConfig(String domain, String path) {
+        this.domain = domain;
+        this.path = path;
+    }
+
+    public HostConfig(PropertyConfig config) {
+        this.domain = config.getStringProperty("domain");
+        this.path = config.getStringProperty("path");
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public String getPath() {
+        return path;
+    }
+}

+ 183 - 0
connector-core/src/main/java/io/connector/core/config/PropertyConfig.java

@@ -0,0 +1,183 @@
+package io.connector.core.config;
+
+import cz.senslog.common.exception.PropertyNotFoundException;
+import cz.senslog.common.util.ClassUtils;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static java.lang.String.format;
+import static java.util.Collections.emptySet;
+import static java.util.Optional.ofNullable;
+
+public class PropertyConfig {
+
+    /** Path delimiter separates nodes. */
+    private static final String PATH_DELIMITER = ".";
+
+    /** Identifier of path. */
+    private final String id;
+
+    /** Map of properties. */
+    private final Map<String, Object> properties;
+
+    /**
+     * Constructor sets new identifier of node.
+     * @param id - identifier of node.
+     */
+    protected PropertyConfig(String id) {
+        this.id = id;
+        this.properties = new HashMap<>();
+    }
+
+    /**
+     * Adds new property to properties.
+     * @param name - name of new property.
+     * @param value - value of new property.
+     */
+    public boolean setProperty(String name, Object value) {
+        Object res = properties.put(name, value);
+        return res == value;
+    }
+
+    /**
+     * Returns value. It could be anything.
+     * @param name - name of property.
+     * @return object of value.
+     */
+    public Object getProperty(String name) {
+        if (properties.containsKey(name)) {
+            return properties.get(name);
+        }
+
+        throw new PropertyNotFoundException(format(
+                "Property '%s' does not exist.", getNewPropertyId(name))
+        );
+    }
+
+    /**
+     * Checks if property key is presents in properties.
+     * @param name - name of property
+     * @return boolean
+     */
+    public boolean containsProperty(String name) {
+        return properties.containsKey(name);
+    }
+
+    /**
+     * Returns optional value. It could be anything.
+     * @param name - name of property.
+     * @return optional object
+     */
+    public Optional<Object> getOptionalProperty(String name) {
+        return ofNullable(properties.get(name));
+    }
+
+    /**
+     * Returns property as a String.
+     * @param name - name of property.
+     * @return string value.
+     */
+    public String getStringProperty(String name) {
+        return ClassUtils.cast(getProperty(name), String.class);
+    }
+
+    /**
+     * Returns property as an Integer.
+     * @param name - name of property.
+     * @return integer value.
+     */
+    public Integer getIntegerProperty(String name) {
+        return ClassUtils.cast(getProperty(name), Integer.class);
+    }
+
+    /**
+     * Returns property as a LocalDateTime.
+     * @param name - name of property.
+     * @return localDateTime value.
+     */
+    public LocalDateTime getLocalDateTimeProperty(String name) {
+        Object object = getProperty(name);
+
+        if (object instanceof LocalDateTime) {
+            return (LocalDateTime) object;
+        } else if (object instanceof Date) {
+            Date date = (Date) object;
+            return date.toInstant().atZone(ZoneOffset.systemDefault()).toLocalDateTime();
+        } else if (object instanceof String) {
+            return LocalDateTime.parse((String)object, DateTimeFormatter.ISO_DATE_TIME);
+
+        } else {
+            throw new ClassCastException(format(
+                    "Property '%s' can not be cast to %s", getNewPropertyId(name), LocalDateTime.class)
+            );
+        }
+    }
+
+    /**
+     * Returns property as a optional of LocalDateTime
+     * @param name - name of property.
+     * @return optional of localDateTime value.
+     */
+    public Optional<LocalDateTime> getOptionalLocalDateTimeProperty(String name) {
+        return properties.containsKey(name) ? Optional.of(getLocalDateTimeProperty(name)) : Optional.empty();
+    }
+
+    /**
+     * Returns property as a set of the 'type'.
+     * @param name - name of property.
+     * @param type - type of attributes
+     * @param <T> - generic type of attribute
+     * @return Set of attributes defined by type
+     */
+    public <T> Set<T> getSetProperty(String name, Class<T> type) {
+        Object value = properties.get(name);
+
+        if (value instanceof  List) {
+            List<?> list = (List<?>) value;
+            Set<T> res = new HashSet<>(list.size());
+            for (Object o : list) {
+                res.add(ClassUtils.cast(o, type));
+            }
+            return res;
+        }
+
+        return emptySet();
+    }
+
+    /**
+     * Returns new node of configuration.
+     * @param name - name of property.
+     * @return node of configuration.
+     */
+    public PropertyConfig getPropertyConfig(String name) {
+        Object property = getProperty(name);
+        PropertyConfig config = new PropertyConfig(getNewPropertyId(name));
+
+        if (property instanceof Map) {
+            Map<?, ?> properties = (Map<?, ?>) property;
+            for (Map.Entry<?, ?> propertyEntry : properties.entrySet()) {
+                Object propertyName = propertyEntry.getKey();
+                if (propertyName instanceof String) {
+                    config.setProperty((String)propertyName, propertyEntry.getValue());
+                }
+            }
+        }
+
+        return config;
+    }
+
+    public Set<String> getAttributes() {
+        return properties.keySet();
+    }
+
+    private String getNewPropertyId(String name) {
+        return id + PATH_DELIMITER + name;
+    }
+
+    public String getId() {
+        return id;
+    }
+}

+ 32 - 0
connector-core/src/main/java/io/connector/core/config/SchedulerConfig.java

@@ -0,0 +1,32 @@
+package io.connector.core.config;
+
+public final class SchedulerConfig {
+
+    private final String id;
+
+    private final Integer period;
+
+    private final String consumer;
+
+    public SchedulerConfig(String id, Integer period, String consumer) {
+        this.id = id;
+        this.period = period;
+        this.consumer = consumer;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public Integer getPeriodMillisecond() {
+        return period * 1000;
+    }
+
+    public Integer getPeriodSecond() {
+        return period;
+    }
+
+    public String getConsumer() {
+        return consumer;
+    }
+}

+ 51 - 0
connector-core/src/main/java/io/connector/core/config/database/DatabaseBuilder.java

@@ -0,0 +1,51 @@
+package io.connector.core.config.database;
+
+
+/**
+ * The interface {@code DatabaseBuilder} provides a configuration
+ * to create new connection to a database.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface DatabaseBuilder {
+
+    static DatabaseBuilder create() {
+        return new DatabaseBuilderImpl();
+    }
+
+    /**
+     * Sets connection url to the configuration.
+     * @param connectionUrl - connection url to the database.
+     * @return instance of builder {@code DatabaseBuilder}.
+     */
+    DatabaseBuilder connectionUrl(String connectionUrl);
+
+    /**
+     * Sets username to the configuration.
+     * @param username - username to the database.
+     * @return instance of builder {@code DatabaseBuilder}.
+     */
+    DatabaseBuilder username(String username);
+
+    /**
+     * Sets password to the configuration.
+     * @param password - password to the database.
+     * @return instance of builder {@code DatabaseBuilder}.
+     */
+    DatabaseBuilder password(String password);
+
+    /**
+     * Identifier of connector name in a configuration database.
+     * @param connectorName connector name.
+     * @return instance of builder {@code DatabaseBuilder}.
+     */
+    DatabaseBuilder connectorName(String connectorName);
+
+    /**
+     * Creates a new instance with the configuration.
+     * @return new instance of {@link DatabaseConfigurationService}.
+     */
+    DatabaseConfigurationService build();
+}

+ 53 - 0
connector-core/src/main/java/io/connector/core/config/database/DatabaseBuilderImpl.java

@@ -0,0 +1,53 @@
+package io.connector.core.config.database;
+
+
+/**
+ * The class {@code DatabaseBuilderImpl} represents an implementation of {@link DatabaseBuilder}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class DatabaseBuilderImpl implements DatabaseBuilder {
+
+    /** Connection url to a database. */
+    private String connectionUrl;
+
+    /** Username to a database. */
+    private String username;
+
+    /** Password to a database. */
+    private String password;
+
+    /** Name of connector in database. */
+    private String connectorName;
+
+    @Override
+    public DatabaseBuilder connectionUrl(String connectionUrl) {
+        this.connectionUrl = connectionUrl;
+        return this;
+    }
+
+    @Override
+    public DatabaseBuilder username(String username) {
+        this.username = username;
+        return this;
+    }
+
+    @Override
+    public DatabaseBuilder password(String password) {
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public DatabaseBuilder connectorName(String connectorName) {
+        this.connectorName = connectorName;
+        return this;
+    }
+
+    @Override
+    public DatabaseConfigurationService build() {
+        return new DatabaseConfigurationServiceImpl(connectionUrl, username, password, connectorName);
+    }
+}

+ 22 - 0
connector-core/src/main/java/io/connector/core/config/database/DatabaseConfigurationService.java

@@ -0,0 +1,22 @@
+package io.connector.core.config.database;
+
+import io.connector.core.config.ConfigurationService;
+
+import java.io.IOException;
+
+/**
+ * The interface {@code DatabaseConfigurationService} provides functionality
+ * which is mandatory only for a database and is important to use it correctly.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface DatabaseConfigurationService extends ConfigurationService {
+
+    /**
+     * Creates a new connection to the database.
+     * @throws IOException throws if the connection is not created successfully.
+     */
+    void connect() throws IOException;
+}

+ 66 - 0
connector-core/src/main/java/io/connector/core/config/database/DatabaseConfigurationServiceImpl.java

@@ -0,0 +1,66 @@
+package io.connector.core.config.database;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.config.ConfigurationService;
+import io.connector.core.config.DefaultConfig;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Optional;
+import java.util.Set;
+
+import static java.lang.String.format;
+
+/**
+ * The class {@code DatabaseConfigurationServiceImpl} represents an implementation of {@link DatabaseConfigurationService}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class DatabaseConfigurationServiceImpl implements ConfigurationService, DatabaseConfigurationService {
+
+    private static Logger logger = LogManager.getLogger(DatabaseConfigurationServiceImpl.class);
+
+    /** Connection url to a database. */
+    private final String connectionUrl;
+
+    /** Username to a database. */
+    private final String username;
+
+    /** Password to a database. */
+    private final String password;
+
+    /** Name of connector in a database. */
+    private final String connectorName;
+
+    /**
+     * Constructor sets all attributes.
+     * @param connectionUrl - connection url.
+     * @param username - username.
+     * @param password - password.
+     */
+    DatabaseConfigurationServiceImpl(String connectionUrl, String username, String password, String connectorName) {
+        this.connectionUrl = connectionUrl;
+        this.username = username;
+        this.password = password;
+        this.connectorName = connectorName;
+    }
+
+    @Override
+    public void connect() {
+        throw logger.throwing(new UnsupportedOperationException(format(
+                "%s#connect() is not implemented.", DatabaseConfigurationServiceImpl.class
+        )));
+    }
+
+    @Override
+    public Set<ModuleDescriptor> getModuleDescriptors() {
+        return null;
+    }
+
+    @Override
+    public Optional<ModuleDescriptor> getModuleDescriptor(String id) {
+        return Optional.empty();
+    }
+}

+ 29 - 0
connector-core/src/main/java/io/connector/core/config/file/FileBuilder.java

@@ -0,0 +1,29 @@
+package io.connector.core.config.file;
+
+/**
+ * The interface {@code FileBuilder} provides a configuration
+ * to load a configuration file.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface FileBuilder {
+
+    static FileBuilder create() {
+        return new FileBuilderImpl();
+    }
+
+    /**
+     * Sets name of file to the configuration.
+     * @param fileName - name of configuration file.
+     * @return instance of builder {@code FileBuilder}.
+     */
+    FileBuilder fileName(String fileName);
+
+    /**
+     * Creates a new instance with the configuration.
+     * @return new instance of {@link FileConfigurationService}.
+     */
+    FileConfigurationService build();
+}

+ 40 - 0
connector-core/src/main/java/io/connector/core/config/file/FileBuilderImpl.java

@@ -0,0 +1,40 @@
+package io.connector.core.config.file;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * The class {@code FileBuilderImpl} represents an implementation of {@link FileBuilder}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class FileBuilderImpl implements FileBuilder {
+
+    private static Logger logger = LogManager.getLogger(FileBuilderImpl.class);
+
+    /** Name of the file configuration. */
+    private String fileName;
+
+    /**
+     * Constructor.
+     */
+    public FileBuilderImpl() {
+        logger.debug("Creating a builder for the configuration service.");
+    }
+
+    @Override
+    public FileBuilder fileName(String fileName) {
+        this.fileName = fileName;
+        return this;
+    }
+
+    @Override
+    public FileConfigurationService build() {
+        logger.debug("Building a new FileConfigurationService");
+        FileConfigurationService service = new FileConfigurationServiceImpl(fileName);
+        logger.debug("FileConfigurationService was build successfully.");
+        return service;
+    }
+}

+ 24 - 0
connector-core/src/main/java/io/connector/core/config/file/FileConfigurationService.java

@@ -0,0 +1,24 @@
+package io.connector.core.config.file;
+
+import io.connector.core.config.ConfigurationService;
+
+import java.io.IOException;
+
+/**
+ * The interface {@code FileConfigurationService} provides functionality
+ * which is mandatory only for a file configuration.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface FileConfigurationService extends ConfigurationService {
+
+    /**
+     * Loads and parses the configuration file.
+     * From the configuration file is loaded configuration for each class
+     * and also connector description which is used to create a new one.
+     * @throws IOException throws if the configuration file is not loaded correctly.
+     */
+    void load() throws IOException;
+}

+ 194 - 0
connector-core/src/main/java/io/connector/core/config/file/FileConfigurationServiceImpl.java

@@ -0,0 +1,194 @@
+package io.connector.core.config.file;
+
+import cz.senslog.common.exception.UnsupportedFileException;
+import cz.senslog.common.util.StringUtils;
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.config.DefaultConfig;
+import io.connector.core.config.SchedulerConfig;
+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.*;
+
+class FileConfigurationServiceImpl  implements FileConfigurationService {
+
+    private static Logger logger = LogManager.getLogger(FileConfigurationServiceImpl.class);
+
+    private Map<String, ModuleDescriptor> moduleDescriptors;
+
+    /** Name of the configuration file. */
+    private final String fileName;
+
+    /**
+     * Constructors sets all attributes.
+     * @param fileName - name of the configuration file.
+     */
+    FileConfigurationServiceImpl(String fileName) {
+        logger.debug("Creating a new FileConfigurationService.");
+        this.fileName = fileName;
+        this.moduleDescriptors = new HashMap<>();
+    }
+
+    @Override
+    public void load() throws IOException {
+        logger.info("Loading '{}' configuration file.", fileName);
+
+        if (!fileName.toLowerCase().endsWith(".yaml")) {
+            throw new UnsupportedFileException(fileName + "does not contain .yaml extension.");
+        }
+
+        Path filePath = Paths.get(fileName);
+        if (Files.notExists(filePath)) {
+            throw new FileNotFoundException(fileName + " does not exist");
+        }
+
+        Map<String, Object> properties;
+
+        logger.debug("Opening the file '{}'.", fileName);
+        try (InputStream fileStream = Files.newInputStream(filePath)) {
+            logger.debug("Parsing the yaml file '{}'.", fileName);
+            properties = new Yaml().load(fileStream);
+            logger.debug("The configuration yaml file '{}' was parsed successfully.", fileName);
+        }
+
+        if (properties == null || properties.isEmpty()) {
+            throw new IOException(String.format(
+                    "The configuration yaml file %s is empty or was not loaded successfully. ", fileName
+            ));
+        }
+
+        logger.debug("Getting 'settings' property from the configuration file.");
+        Map<String, DefaultConfig> serviceSettings = createServiceSettings(properties.get("services"));
+
+        logger.debug("Getting 'scheduler' property from the configuration file.");
+        Map<String, SchedulerConfig> schedulerSettings = createSchedulers(properties.get("scheduler"));
+
+        for (Map.Entry<String, DefaultConfig> serviceEntry : serviceSettings.entrySet()) {
+            String id = serviceEntry.getKey();
+            DefaultConfig config = serviceEntry.getValue();
+            SchedulerConfig scheduler = schedulerSettings.get(id);
+            moduleDescriptors.put(id, new ModuleDescriptor(id, config, scheduler));
+        }
+
+        logger.info("The configuration file '{}' was parsed successfully.", fileName);
+    }
+
+    private Map<String, DefaultConfig> createServiceSettings(Object servicesObject) {
+        if (servicesObject instanceof Map) {
+            Map<?, ?> servicesMap = (Map<?, ?>)servicesObject;
+            Map<String, DefaultConfig> moduleSettings = new HashMap<>(servicesMap.size());
+            for (Map.Entry<?, ?> serviceEntry : servicesMap.entrySet()) {
+                if (!(serviceEntry.getKey() instanceof String)) {
+                    logger.error("service name is not string"); continue;
+                }
+
+                String serviceId = ((String)serviceEntry.getKey()).toLowerCase();
+
+                if (!(serviceEntry.getValue() instanceof Map)) {
+                    logger.error("settings of the service is not dictionary"); continue;
+                }
+                Map<?, ?> serviceSettingMap = (Map<?, ?>)serviceEntry.getValue();
+
+                if (!(serviceSettingMap.containsKey("provider")))  {
+                    logger.error("settings does not contain 'provider' class."); continue;
+                }
+
+                String providerString = (String) serviceSettingMap.remove("provider");
+                if (StringUtils.isBlank(providerString)) {
+                    logger.error(""); continue;
+                }
+
+                Class<?> providerClass = null;
+                try {
+                    logger.debug("Creating a class from the provider class name {}.", providerString);
+                    providerClass = Class.forName(providerString);
+                } catch (ClassNotFoundException e) {
+                    logger.catching(e);
+                }
+
+                DefaultConfig serviceConfig = new DefaultConfig(serviceId, providerClass);
+
+                for (Map.Entry<?, ?> serviceSettingEntry : serviceSettingMap.entrySet()) {
+                    if (!(serviceSettingEntry.getKey() instanceof String)) {
+                        logger.warn(""); continue;
+                    }
+                    serviceConfig.setProperty((String)serviceSettingEntry.getKey(), serviceSettingEntry.getValue());
+                }
+
+                moduleSettings.put(serviceId, serviceConfig);
+            }
+            return moduleSettings;
+        }
+
+        return Collections.emptyMap();
+    }
+
+    private Map<String, SchedulerConfig> createSchedulers(Object schedulerObject) {
+        if (schedulerObject instanceof Map) {
+            Map<?, ?> schedulerMap = (Map<?, ?>) schedulerObject;
+            Map<String, SchedulerConfig> schedulerSettings = new HashMap<>(schedulerMap.size());
+            for (Map.Entry<?, ?> schedulerEntry : schedulerMap.entrySet()) {
+                if (!(schedulerEntry.getKey() instanceof String)) {
+                    logger.error("scheduler name is not string"); continue;
+                }
+
+                String serviceId = ((String) schedulerEntry.getKey()).toLowerCase();
+
+                if (!(schedulerEntry.getValue() instanceof Map)) {
+                    logger.error("settings of the service is not dictionary"); continue;
+                }
+
+                Map<?, ?> scheduleSettMap = (Map<?, ?>) schedulerEntry.getValue();
+
+                if(!(scheduleSettMap.containsKey("period"))) {
+                    logger.error(""); continue;
+                }
+
+                if (!(scheduleSettMap.containsKey("consumer"))) {
+                    logger.error(""); continue;
+                }
+
+                Object periodObject = scheduleSettMap.get("period");
+                assert periodObject instanceof Integer;
+                Integer period = (Integer) periodObject;
+
+                if (period <= 0) {
+                    logger.warn(""); continue;
+                }
+
+                Object consumerObject = scheduleSettMap.get("consumer");
+                assert consumerObject instanceof String;
+                String consumer = (String)consumerObject;
+
+                if (StringUtils.isBlank(consumer)) {
+                    logger.warn(""); continue;
+                }
+
+                SchedulerConfig schedulerConfig = new SchedulerConfig(serviceId, period, consumer);
+
+                schedulerSettings.put(serviceId, schedulerConfig);
+            }
+
+            return schedulerSettings;
+        }
+
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public Set<ModuleDescriptor> getModuleDescriptors() {
+        return new HashSet<>(moduleDescriptors.values());
+    }
+
+    @Override
+    public Optional<ModuleDescriptor> getModuleDescriptor(String id) {
+        return Optional.ofNullable(moduleDescriptors.get(id));
+    }
+}

+ 6 - 0
connector-core/src/main/java/io/connector/core/json/DeserializeFormatter.java

@@ -0,0 +1,6 @@
+package io.connector.core.json;
+
+@FunctionalInterface
+public interface DeserializeFormatter<T> {
+    T apply(String element);
+}

+ 41 - 0
connector-core/src/main/java/io/connector/core/json/JsonAttributeFormatter.java

@@ -0,0 +1,41 @@
+package io.connector.core.json;
+
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public interface JsonAttributeFormatter {
+
+    static JsonAttributeFormatter builder() {
+        return new JsonAttributeFormatterImpl();
+    }
+
+    <E> JsonAttributeFormatter addSerializer(Class<E> aClass, SerializeFormatter<E> formatter);
+    <E> JsonAttributeFormatter addDeserializer(Class<E> aClass, DeserializeFormatter<E> formatter);
+
+    SimpleModule getModule();
+
+    class JsonAttributeFormatterImpl implements JsonAttributeFormatter {
+
+        private final SimpleModule module;
+
+        private JsonAttributeFormatterImpl() {
+            module = new SimpleModule();
+        }
+
+        @Override
+        public <E> JsonAttributeFormatter addSerializer(Class<E> aClass, SerializeFormatter<E> formatter) {
+            module.addSerializer(aClass, new JsonSerializer<>(aClass, formatter));
+            return this;
+        }
+
+        @Override
+        public <E> JsonAttributeFormatter addDeserializer(Class<E> aClass, DeserializeFormatter<E> formatter) {
+            module.addDeserializer(aClass, new JsonDeserializer<>(aClass, formatter));
+            return this;
+        }
+
+        @Override
+        public SimpleModule getModule() {
+            return module;
+        }
+    }
+}

+ 23 - 0
connector-core/src/main/java/io/connector/core/json/JsonDeserializer.java

@@ -0,0 +1,23 @@
+package io.connector.core.json;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+import java.io.IOException;
+
+final class JsonDeserializer<T> extends StdDeserializer<T> {
+
+    private final DeserializeFormatter<T> formatter;
+
+    protected JsonDeserializer(Class<T> t, DeserializeFormatter<T> formatter) {
+        super(t);
+        this.formatter = formatter;
+    }
+
+
+    @Override
+    public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+        return formatter.apply(jsonParser.getText());
+    }
+}

+ 22 - 0
connector-core/src/main/java/io/connector/core/json/JsonSerializer.java

@@ -0,0 +1,22 @@
+package io.connector.core.json;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+import java.io.IOException;
+
+final class JsonSerializer<T> extends StdSerializer<T> {
+
+    private final SerializeFormatter<T> formatter;
+
+    protected JsonSerializer(Class<T> t, SerializeFormatter<T> formatter) {
+        super(t);
+        this.formatter = formatter;
+    }
+
+    @Override
+    public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+        jsonGenerator.writeString(formatter.apply(t));
+    }
+}

+ 6 - 0
connector-core/src/main/java/io/connector/core/json/SerializeFormatter.java

@@ -0,0 +1,6 @@
+package io.connector.core.json;
+
+@FunctionalInterface
+public interface SerializeFormatter<T> {
+    String apply(T element);
+}

+ 59 - 0
connector-core/src/main/java/io/connector/core/module/AbstractModule.java

@@ -0,0 +1,59 @@
+package io.connector.core.module;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.ModuleDescriptor;
+import io.vertx.core.AbstractVerticle;
+import io.vertx.ext.web.Router;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static io.connector.core.AddressPath.Creator.create;
+import static io.connector.core.AddressPath.INFO;
+
+public abstract class AbstractModule extends AbstractVerticle {
+
+    protected final ModuleDescriptor descriptor;
+    public final ModuleType type;
+    private final Map<String, AbstractGateway> gateways;
+
+    protected AbstractModule(ModuleDescriptor descriptor, ModuleType type) {
+        this.descriptor = descriptor;
+        this.type = type;
+        this.gateways = new HashMap<>();
+    }
+
+
+    protected void registerGateway(AbstractGateway gateway) {
+        if (!gateways.containsKey(gateway.id())) {
+            gateways.put(gateway.id(), gateway);
+        }
+    }
+
+    public abstract void run() throws Exception;
+
+    public abstract ModuleInfo info();
+
+    public String id() {
+        return type.name().toLowerCase();
+    }
+
+    public ModuleDescriptor descriptor() {
+        return descriptor;
+    }
+
+    public Router router() {
+        Router moduleRouter = Router.router(vertx);
+        for (AbstractGateway gateway : gateways.values()) {
+            moduleRouter.mountSubRouter(create(gateway.id()), gateway.router());
+        }
+        return moduleRouter;
+    }
+
+    @Override
+    public void start() throws Exception {
+        run();
+        gateways.values().forEach(AbstractGateway::start);
+        vertx.eventBus().consumer(INFO, msg -> msg.reply(info()));
+    }
+}

+ 59 - 0
connector-core/src/main/java/io/connector/core/module/ModuleInfo.java

@@ -0,0 +1,59 @@
+package io.connector.core.module;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.MessageCodec;
+import io.vertx.core.json.JsonObject;
+
+public class ModuleInfo {
+
+    private final String id;
+
+    public ModuleInfo(String id) {
+        this.id = id;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public static MessageCodec<ModuleInfo, ModuleInfo> createCodec() {
+        return new MessageCodec<>() {
+            @Override
+            public void encodeToWire(Buffer buffer, ModuleInfo moduleInfo) {
+                JsonObject jsonToEncode = new JsonObject();
+                jsonToEncode.put("id", moduleInfo.getId());
+                String jsonToStr = jsonToEncode.encode();
+                int length = jsonToStr.getBytes().length;
+                buffer.appendInt(length);
+                buffer.appendString(jsonToStr);
+            }
+
+            @Override
+            public ModuleInfo decodeFromWire(int i, Buffer buffer) {
+                int _pos = i;
+                int length = buffer.getInt(_pos);
+
+                String jsonStr = buffer.getString(_pos += 4, _pos += length);
+                JsonObject contentJson = new JsonObject(jsonStr);
+
+                String id = contentJson.getString("id");
+                return new ModuleInfo(id);
+            }
+
+            @Override
+            public ModuleInfo transform(ModuleInfo moduleInfo) {
+                return moduleInfo;
+            }
+
+            @Override
+            public String name() {
+                return this.getClass().getName();
+            }
+
+            @Override
+            public byte systemCodecID() {
+                return -1;
+            }
+        };
+    }
+}

+ 41 - 0
connector-core/src/main/java/io/connector/core/module/ModuleLoader.java

@@ -0,0 +1,41 @@
+package io.connector.core.module;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.config.ConfigurationService;
+import io.connector.core.config.DefaultConfig;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+
+public final class ModuleLoader {
+
+    private static Logger logger = LogManager.getLogger(ModuleLoader.class);
+
+    public static List<AbstractModule> loadModules(ConfigurationService configService) {
+
+        Map<Class<? extends ModuleProvider>, ModuleProvider> moduleProviders = new HashMap<>();
+        ServiceLoader<ModuleProvider> loader = ServiceLoader.load(ModuleProvider.class);
+
+        for (ModuleProvider provider : loader) {
+            moduleProviders.put(provider.getClass(), provider);
+        }
+
+
+        List<AbstractModule> modules = new ArrayList<>(moduleProviders.size());
+
+        Set<ModuleDescriptor> moduleDescriptors = configService.getModuleDescriptors();
+        for (ModuleDescriptor descriptor : moduleDescriptors) {
+            DefaultConfig config = descriptor.getServiceConfig();
+            ModuleProvider moduleProvider = moduleProviders.get(config.getProviderClass());
+
+            if (moduleProvider != null) {
+                AbstractModule module = moduleProvider.createModule(descriptor);
+
+                modules.add(module);
+            }
+        }
+
+        return modules;
+    }
+}

+ 8 - 0
connector-core/src/main/java/io/connector/core/module/ModuleProvider.java

@@ -0,0 +1,8 @@
+package io.connector.core.module;
+
+import io.connector.core.ModuleDescriptor;
+
+public interface ModuleProvider {
+
+    AbstractModule createModule(ModuleDescriptor descriptor);
+}

+ 11 - 0
connector-core/src/main/java/io/connector/core/module/ModuleType.java

@@ -0,0 +1,11 @@
+package io.connector.core.module;
+
+public enum ModuleType {
+
+    SENSLOG1,
+    AFARCLOUD,
+    IMA,
+    OGC_SENSOR_THINGS,
+
+    ;
+}

+ 0 - 0
connector-model/build.gradle


+ 22 - 0
connector-model/src/main/java/io/connector/model/afarcloud/AFCModel.java

@@ -0,0 +1,22 @@
+package io.connector.model.afarcloud;
+
+import io.vertx.core.json.JsonObject;
+
+public class AFCModel {
+
+    private final String data;
+
+    public static AFCModel parse(JsonObject jsonObject) {
+        // TODO check by schema
+        String data = jsonObject.getString("data");
+        return new AFCModel(data);
+    }
+
+    public AFCModel(String data) {
+        this.data = data;
+    }
+
+    public String getData() {
+        return data;
+    }
+}

+ 6 - 0
connector-model/src/main/java/io/connector/model/afarcloud/Observation.java

@@ -0,0 +1,6 @@
+package io.connector.model.afarcloud;
+
+public class Observation {
+
+
+}

+ 14 - 0
connector-model/src/main/java/io/connector/model/ima/IMAModel.java

@@ -0,0 +1,14 @@
+package io.connector.model.ima;
+
+public class IMAModel {
+
+    private final String data;
+
+    public IMAModel(String data) {
+        this.data = data;
+    }
+
+    public String getData() {
+        return data;
+    }
+}

+ 113 - 0
connector-model/src/main/java/io/connector/model/senslog1/Observation.java

@@ -0,0 +1,113 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.MessageCodec;
+import io.vertx.core.json.JsonObject;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class Observation {
+
+    private final OffsetDateTime timestamp;
+    private final double value;
+
+    public static Observation parse(JsonObject jsonObject) {
+        // TODO check by schema
+
+        double value;
+        if (jsonObject.containsKey("value")) {
+            value = jsonObject.getDouble("value");
+        } else if (jsonObject.containsKey("observedValue")) {
+            value = jsonObject.getDouble("observedValue");
+        } else {
+            return null;
+        }
+
+        OffsetDateTime timestamp;
+        if (jsonObject.containsKey("time")) {
+            final DateTimeFormatter formatter = ofPattern("yyyy-MM-dd HH:mm:ssZ");
+            String timeString = jsonObject.getString("time");
+            timestamp = OffsetDateTime.parse(timeString + "00", formatter);
+        } else if (jsonObject.containsKey("timeStamp")) {
+            final DateTimeFormatter formatter = ofPattern("yyyy-MM-dd HH:mm:ssZ");
+            String timeString = jsonObject.getString("timeStamp");
+            timestamp = OffsetDateTime.parse(timeString + "00", formatter);
+        } else if (jsonObject.containsKey("timestamp")) {
+            String timeString = jsonObject.getString("timestamp");
+            timestamp = OffsetDateTime.parse(timeString, ISO_OFFSET_DATE_TIME);
+        } else {
+            return null;
+        }
+
+        return new Observation(value, timestamp);
+    }
+
+    public Observation(double value, OffsetDateTime timestamp) {
+        Objects.requireNonNull(timestamp);
+        this.value = value;
+        this.timestamp = timestamp;
+    }
+
+    public double getValue() {
+        return value;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public static MessageCodec<Observation, Observation> createCodec() {
+        return new MessageCodec<>() {
+
+            @Override
+            public void encodeToWire(Buffer buffer, Observation entries) {
+                JsonObject jsonToEncode = new JsonObject();
+                jsonToEncode.put("value", entries.getValue());
+                jsonToEncode.put("timestamp", entries.getTimestamp().format(ISO_OFFSET_DATE_TIME));
+
+                String jsonToStr = jsonToEncode.encode();
+                int length = jsonToStr.getBytes().length;
+
+                buffer.appendInt(length);
+                buffer.appendString(jsonToStr);
+            }
+
+            @Override
+            public Observation decodeFromWire(int i, Buffer buffer) {
+                int _pos = i;
+                int length = buffer.getInt(_pos);
+
+                String jsonStr = buffer.getString(_pos += 4, _pos += length);
+                JsonObject contentJson = new JsonObject(jsonStr);
+
+                double value = contentJson.getDouble("value");
+                String timestampString = contentJson.getString("timestamp");
+                OffsetDateTime timestamp = OffsetDateTime.parse(timestampString, ISO_OFFSET_DATE_TIME);
+
+                return new Observation(value, timestamp);
+            }
+
+            @Override
+            public Observation transform(Observation observation) {
+                return observation;
+            }
+
+            @Override
+            public String name() {
+                return this.getClass().getName();
+            }
+
+            @Override
+            public byte systemCodecID() {
+                return -1;
+            }
+        };
+    }
+}
+
+

+ 36 - 0
connector-model/src/main/java/io/connector/model/senslog1/Phenomenon.java

@@ -0,0 +1,36 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.json.JsonObject;
+
+public class Phenomenon {
+
+    private final long id;
+    private final String name;
+    private final String unit;
+
+    public static Phenomenon parse(JsonObject jsonObject) {
+        // TODO check by schema
+        long id = Long.parseLong(jsonObject.getString("phenomenonId"));
+        String name = jsonObject.getString("phenomenonName");
+        String unit = jsonObject.getString("unit");
+        return new Phenomenon(id, name, unit);
+    }
+
+    public Phenomenon(long id, String name, String unit) {
+        this.id = id;
+        this.name = name;
+        this.unit = unit;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+}

+ 65 - 0
connector-model/src/main/java/io/connector/model/senslog1/Position.java

@@ -0,0 +1,65 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.json.JsonObject;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class Position {
+
+    private final Double latitude;
+    private final Double longitude;
+    private final Double altitude;
+    private final Double speed;
+    private final Integer dilutionOfPrecision;
+    private final OffsetDateTime timestamp;
+
+    public static Position parse(JsonObject jsonObject) {
+        // TODO check by schema
+        double x = jsonObject.getDouble("x");
+        double y = jsonObject.getDouble("y");
+        String timeString = jsonObject.getString("time_stamp");
+        final DateTimeFormatter formatter = ofPattern("yyyy-MM-dd HH:mm:ssZ");
+        OffsetDateTime time = OffsetDateTime.parse(timeString+"00", formatter);
+        return new Position(x, y, time);
+    }
+
+    private Position(Double latitude, Double longitude, Double altitude, Double speed, Integer dilutionOfPrecision, OffsetDateTime timestamp) {
+        this.latitude = latitude;
+        this.longitude = longitude;
+        this.altitude = altitude;
+        this.speed = speed;
+        this.dilutionOfPrecision = dilutionOfPrecision;
+        this.timestamp = timestamp;
+    }
+
+    public Position(double latitude, double longitude, OffsetDateTime timestamp) {
+        this(latitude, longitude, null, null, null, timestamp);
+    }
+
+    public Double getLatitude() {
+        return latitude;
+    }
+
+    public Double getLongitude() {
+        return longitude;
+    }
+
+    public Double getAltitude() {
+        return altitude;
+    }
+
+    public Double getSpeed() {
+        return speed;
+    }
+
+    public Integer getDilutionOfPrecision() {
+        return dilutionOfPrecision;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+}

+ 90 - 0
connector-model/src/main/java/io/connector/model/senslog1/SensorInfo.java

@@ -0,0 +1,90 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.json.JsonObject;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+import static java.lang.String.format;
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class SensorInfo {
+
+    private final long id;
+    private final String name;
+    private final String type;
+    private final Phenomenon phenomenon;
+
+    private final OffsetDateTime firstObservationTime;
+    private final OffsetDateTime lastObservationTime;
+
+    public static SensorInfo parse(JsonObject jsonObject) {
+        // TODO check by schema
+        long id = jsonObject.getLong("sensorId");
+        String name = jsonObject.getString("sensorName");
+        String type = jsonObject.getString("sensorType");
+        Phenomenon phenomenon = Phenomenon.parse(jsonObject.getJsonObject("phenomenon"));
+
+        final DateTimeFormatter formatter = ofPattern("yyyy-MM-dd HH:mm:ssZ");
+        String firstTimeStr = jsonObject.getString("firstObservationTime");
+        OffsetDateTime firstTime = OffsetDateTime.parse(format("%s00", firstTimeStr), formatter);
+        String lastTimeStr = jsonObject.getString("lastObservationTime");
+        OffsetDateTime lastTime = OffsetDateTime.parse(format("%s00", lastTimeStr), formatter);
+
+        return new SensorInfo(id, name, type, phenomenon, firstTime, lastTime);
+    }
+
+    public SensorInfo(long id, String name, String type, Phenomenon phenomenon,
+                      OffsetDateTime firstObservationTime, OffsetDateTime lastObservationTime)
+    {
+        Objects.requireNonNull(name);
+        Objects.requireNonNull(type);
+        Objects.requireNonNull(phenomenon);
+        Objects.requireNonNull(firstObservationTime);
+        Objects.requireNonNull(lastObservationTime);
+        this.id = id;
+        this.name = name;
+        this.type = type;
+        this.phenomenon = phenomenon;
+        this.firstObservationTime = firstObservationTime;
+        this.lastObservationTime = lastObservationTime;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public Phenomenon getPhenomenon() {
+        return phenomenon;
+    }
+
+    public OffsetDateTime getFirstObservationTime() {
+        return firstObservationTime;
+    }
+
+    public OffsetDateTime getLastObservationTime() {
+        return lastObservationTime;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SensorInfo sensorInfo = (SensorInfo) o;
+        return Objects.equals(id, sensorInfo.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+}

+ 120 - 0
connector-model/src/main/java/io/connector/model/senslog1/SensorObservation.java

@@ -0,0 +1,120 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.MessageCodec;
+import io.vertx.core.json.Json;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class SensorObservation {
+
+    private final long id;
+    private final List<Observation> observations;
+
+    public static SensorObservation parse(JsonObject jsonObject) {
+        // TODO check by schema
+        long id = jsonObject.getLong("id");
+        JsonArray observationsJson = jsonObject.getJsonArray("observations");
+        List<Observation> observations = new ArrayList<>(observationsJson.size());
+        for (Object observation : observationsJson) {
+            if (observation instanceof JsonObject) {
+                observations.add(Observation.parse((JsonObject)observation));
+            }
+        }
+        return new SensorObservation(id, observations);
+    }
+
+    public SensorObservation(long id) {
+        this(id, new ArrayList<>());
+    }
+
+    public SensorObservation(long id, List<Observation> observations) {
+        Objects.requireNonNull(observations);
+        this.id = id;
+        this.observations = observations;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void addObservation(Observation observation) {
+        this.observations.add(observation);
+    }
+
+    public List<Observation> getObservations() {
+        return observations;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SensorObservation sensorObservation = (SensorObservation) o;
+        return Objects.equals(getId(), sensorObservation.getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getId());
+    }
+
+    public static MessageCodec<SensorObservation, SensorObservation> createCodec() {
+        return new MessageCodec<>() {
+
+            @Override
+            public void encodeToWire(Buffer buffer, SensorObservation sensor) {
+                JsonObject jsonToEncode = new JsonObject();
+                jsonToEncode.put("sensorId", sensor.getId());
+                jsonToEncode.put("observations", Json.encode(sensor.getObservations()));
+
+                String jsonToStr = jsonToEncode.encode();
+                int length = jsonToStr.getBytes().length;
+
+                buffer.appendInt(length);
+                buffer.appendString(jsonToStr);
+            }
+
+            @Override
+            public SensorObservation decodeFromWire(int i, Buffer buffer) {
+                int _pos = i;
+                int length = buffer.getInt(_pos);
+
+                String jsonStr = buffer.getString(_pos += 4, _pos += length);
+                JsonObject contentJson = new JsonObject(jsonStr);
+
+                long sensorId = contentJson.getLong("sensorId");
+
+                /*
+                String jsonObservationStr = contentJson.getString("observations");
+                JsonArray jsonObservationsArr = contentJson.getJsonArray("observations");
+
+                // TODO
+                TypeReference<List<Observation>> ref = new TypeReference<>() {};
+                List<Observation> observations = Json.decodeValue(jsonObservationStr, ref);
+                */
+
+                return new SensorObservation(sensorId, new ArrayList<>());
+            }
+
+            @Override
+            public SensorObservation transform(SensorObservation sensor) {
+                return sensor;
+            }
+
+            @Override
+            public String name() {
+                return this.getClass().getName();
+            }
+
+            @Override
+            public byte systemCodecID() {
+                return -1;
+            }
+        };
+    }
+}

+ 175 - 0
connector-model/src/main/java/io/connector/model/senslog1/UnitData.java

@@ -0,0 +1,175 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.MessageCodec;
+import io.vertx.core.json.Json;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+import java.util.*;
+
+public class UnitData {
+
+    private final long id;
+
+    private Map<Long, SensorObservation> sensorCache;
+    private List<Position> positions;
+
+    public static UnitData parse(JsonObject jsonObject) {
+        // TODO check by schema
+        long id = jsonObject.getLong("id");
+        JsonArray positionsJson = jsonObject.getJsonArray("positions");
+        List<Position> positions = new ArrayList<>(positionsJson.size());
+        for (Object position : positionsJson) {
+            if (position instanceof JsonObject) {
+                positions.add(Position.parse((JsonObject)position));
+            }
+        }
+        JsonArray sensorsJson = jsonObject.getJsonArray("sensors");
+        List<SensorObservation> sensors = new ArrayList<>(sensorsJson.size());
+        for (Object sensor : sensorsJson) {
+            if (sensor instanceof JsonObject) {
+                sensors.add(SensorObservation.parse((JsonObject)sensor));
+            }
+        }
+        return new UnitData(id, sensors, positions);
+    }
+
+    public UnitData(long id) {
+        this(id, new ArrayList<>(), new ArrayList<>());
+    }
+
+    public UnitData(long id, List<SensorObservation> sensors, List<Position> positions) {
+        this.id = id;
+        setSensors(sensors);
+        setPositions(positions);
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setSensors(Collection<SensorObservation> sensors) {
+        this.sensorCache = new HashMap<>(sensors.size());
+        for (SensorObservation sensor : sensors) {
+            sensorCache.put(sensor.getId(), sensor);
+        }
+    }
+
+    public void addSensor(SensorObservation sensor) {
+        Objects.requireNonNull(sensor);
+        sensorCache.put(sensor.getId(), sensor);
+    }
+
+    public SensorObservation getSensor(long sensorId) {
+        if (sensorCache.containsKey(sensorId)) {
+            return sensorCache.get(sensorId);
+        }
+        return null;
+    }
+
+    public Collection<SensorObservation> getSensors() {
+        return sensorCache.values();
+    }
+
+    public void setPositions(List<Position> positions) {
+        Objects.requireNonNull(positions);
+        this.positions = positions;
+    }
+
+    public void addPosition(Position position) {
+        this.positions.add(position);
+    }
+
+    public List<Position> getPositions() {
+        return positions;
+    }
+
+    public final void mergeIn(UnitData unitData) {
+        if (this.id != unitData.getId()) { return; }
+
+        Collection<SensorObservation> sensors = unitData.getSensors();
+        if (this.sensorCache.isEmpty() && !sensors.isEmpty()) {
+            setSensors(sensors);
+        }
+
+        List<Position> positions = unitData.getPositions();
+        if (this.positions.isEmpty()) {
+            setPositions(positions);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        UnitData unitData = (UnitData) o;
+        return Objects.equals(getId(), unitData.getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getId());
+    }
+
+    public static MessageCodec<UnitData, UnitData> createCodec() {
+        return new MessageCodec<>() {
+
+            @Override
+            public void encodeToWire(Buffer buffer, UnitData unit) {
+                JsonObject jsonToEncode = new JsonObject();
+                jsonToEncode.put("unitId", unit.getId());
+                jsonToEncode.put("sensors", Json.encode(unit.getSensors()));
+                jsonToEncode.put("positions", Json.encode(unit.getPositions()));
+
+                String jsonToStr = jsonToEncode.encode();
+                int length = jsonToStr.getBytes().length;
+
+                buffer.appendInt(length);
+                buffer.appendString(jsonToStr);
+            }
+
+            @Override
+            public UnitData decodeFromWire(int i, Buffer buffer) {
+                int _pos = i;
+                int length = buffer.getInt(_pos);
+
+                String jsonStr = buffer.getString(_pos += 4, _pos += length);
+                JsonObject contentJson = new JsonObject(jsonStr);
+
+                long unitId = contentJson.getLong("unitId");
+
+                /*
+                String jsonSensorsStr = contentJson.getString("sensors");
+                JsonArray jsonSensorsArr = contentJson.getJsonArray("sensors");
+                // TODO
+                TypeReference<List<SensorObservation>> refSensors = new TypeReference<>() {};
+                List<SensorObservation> sensors = Json.decodeValue(jsonSensorsStr, refSensors);
+
+                String jsonPositionsStr = contentJson.getString("positions");
+                JsonArray jsonPositionsArr = contentJson.getJsonArray("positions");
+                // TODO
+                TypeReference<List<Position>> refPosition = new TypeReference<>() {};
+                List<Position> positions = Json.decodeValue(jsonPositionsStr, refPosition);
+                */
+
+                return new UnitData(unitId, new ArrayList<>(), new ArrayList<>());
+            }
+
+            @Override
+            public UnitData transform(UnitData unitData) {
+                return unitData;
+            }
+
+            @Override
+            public String name() {
+                return this.getClass().getSimpleName();
+            }
+
+            @Override
+            public byte systemCodecID() {
+                return -1;
+            }
+        };
+    }
+}

+ 84 - 0
connector-model/src/main/java/io/connector/model/senslog1/UnitInfo.java

@@ -0,0 +1,84 @@
+package io.connector.model.senslog1;
+
+import io.vertx.core.json.JsonObject;
+
+import java.util.*;
+
+public class UnitInfo {
+
+    private final long id;
+    private final long holderId;
+    private final String description;
+
+    private Map<Long, SensorInfo> sensorCache;
+
+    public static UnitInfo parse(JsonObject jsonObject) {
+        // TODO check by schema
+        long id = jsonObject.getLong("unitId");
+        int holderId = jsonObject.getInteger("holderId");
+        String description = jsonObject.getString("description");
+
+        return new UnitInfo(id, holderId, description);
+    }
+
+    public UnitInfo(long unitId, int holderId, String description) {
+        this(unitId, holderId, description, new ArrayList<>());
+    }
+
+    public UnitInfo(long unitId, int holderId, String description, List<SensorInfo> sensors) {
+        Objects.requireNonNull(description);
+        this.id = unitId;
+        this.holderId = holderId;
+        this.description = description;
+        setSensors(sensors);
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public long getHolderId() {
+        return holderId;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void addSensor(SensorInfo sensor) {
+        Objects.requireNonNull(sensor);
+        sensorCache.put(sensor.getId(), sensor);
+    }
+
+    public void setSensors(List<SensorInfo> sensors) {
+        Objects.requireNonNull(sensors);
+        this.sensorCache = new HashMap<>(sensors.size());
+        for (SensorInfo sensor : sensors) {
+            sensorCache.put(sensor.getId(), sensor);
+        }
+    }
+
+    public Collection<SensorInfo> getSensors() {
+        return sensorCache.values();
+    }
+
+    public SensorInfo getSensor(long sensorId) {
+        if (sensorCache.containsKey(sensorId)) {
+            return sensorCache.get(sensorId);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        UnitInfo unitInfo = (UnitInfo) o;
+        return Objects.equals(id, unitInfo.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+}

+ 0 - 0
connector-module-afarcloud/build.gradle


+ 25 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCClient.java

@@ -0,0 +1,25 @@
+package io.connector.module.afarcloud;
+
+import io.connector.model.afarcloud.AFCModel;
+import io.connector.module.afarcloud.gateway.SensLog1Gateway;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.List;
+
+public class AFCClient {
+
+    private final static Logger logger = LogManager.getLogger(SensLog1Gateway.class);
+
+    private final AFCConfig config;
+
+    public AFCClient(AFCConfig config) {
+        this.config = config;
+    }
+
+    public void uploadObservations(List<AFCModel> observations) {
+        logger.info("Converted {} observations.", observations.size());
+    }
+}

+ 10 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCConfig.java

@@ -0,0 +1,10 @@
+package io.connector.module.afarcloud;
+
+import io.connector.core.config.DefaultConfig;
+
+public class AFCConfig {
+
+    AFCConfig(DefaultConfig defaultConfig) {
+
+    }
+}

+ 31 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCModule.java

@@ -0,0 +1,31 @@
+package io.connector.module.afarcloud;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleInfo;
+import io.connector.core.module.ModuleType;
+import io.connector.module.afarcloud.gateway.AFCGateway;
+import io.connector.module.afarcloud.gateway.SensLog1Gateway;
+import io.vertx.ext.web.Router;
+
+
+public class AFCModule extends AbstractModule {
+
+    private final AFCClient client;
+
+    public AFCModule(ModuleDescriptor descriptor, AFCClient client) {
+        super(descriptor, ModuleType.AFARCLOUD);
+        this.client = client;
+    }
+
+    @Override
+    public void run() {
+        registerGateway(new AFCGateway(vertx.eventBus(), Router.router(vertx), type, client));
+        registerGateway(new SensLog1Gateway(vertx.eventBus(), Router.router(vertx), type, client));
+    }
+
+    @Override
+    public ModuleInfo info() {
+        return new ModuleInfo(id());
+    }
+}

+ 17 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCModuleProvider.java

@@ -0,0 +1,17 @@
+package io.connector.module.afarcloud;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleProvider;
+
+public final class AFCModuleProvider implements ModuleProvider {
+
+    @Override
+    public AbstractModule createModule(ModuleDescriptor descriptor) {
+
+        AFCConfig config = new AFCConfig(descriptor.getServiceConfig());
+        AFCClient client = new AFCClient(config);
+
+        return new AFCModule(descriptor, client);
+    }
+}

+ 18 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/UnitCollection.java

@@ -0,0 +1,18 @@
+package io.connector.module.afarcloud;
+
+import io.vertx.core.json.JsonArray;
+
+public class UnitCollection extends JsonArray {
+
+    public static class Unit {
+        public String stationId;
+
+        public Unit(String stationId) {
+            this.stationId = stationId;
+        }
+    }
+
+    public void addUnit(Unit unit) {
+        this.add(unit);
+    }
+}

+ 24 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/gateway/AFCGateway.java

@@ -0,0 +1,24 @@
+package io.connector.module.afarcloud.gateway;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.module.ModuleType;
+import io.connector.module.afarcloud.AFCClient;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.ext.web.Router;
+
+
+import static io.connector.core.module.ModuleType.AFARCLOUD;
+
+public class AFCGateway extends AbstractGateway {
+
+    private final AFCClient client;
+
+    public AFCGateway(EventBus eventBus, Router router, ModuleType moduleType, AFCClient client) {
+        super(eventBus, router, moduleType, AFARCLOUD);
+        this.client = client;
+    }
+
+    @Override
+    public void run() {
+    }
+}

+ 101 - 0
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/gateway/SensLog1Gateway.java

@@ -0,0 +1,101 @@
+package io.connector.module.afarcloud.gateway;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.DataCollection;
+import io.connector.core.MessageHeader;
+import io.connector.core.module.ModuleType;
+import io.connector.model.afarcloud.AFCModel;
+import io.connector.model.senslog1.Observation;
+import io.connector.model.senslog1.SensorObservation;
+import io.connector.model.senslog1.UnitData;
+import io.connector.module.afarcloud.AFCClient;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static io.connector.core.module.ModuleType.SENSLOG1;
+
+public class SensLog1Gateway extends AbstractGateway {
+
+    private final static Logger logger = LogManager.getLogger(SensLog1Gateway.class);
+
+    private final AFCClient client;
+
+    public SensLog1Gateway(EventBus eventBus, Router router, ModuleType moduleType, AFCClient client) {
+        super(eventBus, router, moduleType, SENSLOG1);
+        this.client = client;
+    }
+
+
+    @Override
+    public void run() {
+
+        schedulerMapping()
+                .addMapping("observations", "add-observations");
+
+
+        publicConsumer("add-observations", message -> {
+            String resource = message.headers().get(MessageHeader.RESOURCE);
+
+            Iterator<Object> dataIterator;
+            switch (resource) {
+                case MessageHeader.Resource.HTTP_SERVER: {
+                    dataIterator = (((Buffer)message.body()).toJsonArray()).iterator();
+                } break;
+                case MessageHeader.Resource.SCHEDULER: {
+                    dataIterator = ((DataCollection<?>)message.body()).iterator();
+                } break;
+                default: {
+                    message.fail(400, "Unknown resource of data."); return;
+                }
+            }
+
+            List<AFCModel> afcObservations = new ArrayList<>();
+            while (dataIterator.hasNext()) {
+                Object data = dataIterator.next();
+                UnitData unitData = null;
+                if (data instanceof JsonObject) {
+                    unitData = UnitData.parse((JsonObject)data);
+                } else if (data instanceof UnitData) {
+                    unitData = (UnitData)data;
+                }
+
+                if (unitData != null) {
+                    afcObservations.addAll(Converter.convertObservation(unitData));
+                }
+            }
+
+            client.uploadObservations(afcObservations);
+
+            message.reply(new JsonObject().put("message",
+                    String.format("Added %s observations.", afcObservations.size())
+            ));
+        });
+
+    }
+
+    private static class Converter {
+
+        public static List<AFCModel> convertObservation(UnitData unitObservations) {
+            List<AFCModel> afcObservations = new ArrayList<>();
+
+            for (SensorObservation sensor : unitObservations.getSensors()) {
+                for (Observation observation : sensor.getObservations()) {
+                    afcObservations.add(new AFCModel(
+                            String.format("%s_%s_%s", unitObservations.getId(), sensor.getId(), observation.getValue()))
+                    );
+                }
+            }
+
+            return afcObservations;
+        }
+
+    }
+}

+ 1 - 0
connector-module-afarcloud/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider

@@ -0,0 +1 @@
+io.connector.module.afarcloud.AFCModuleProvider

+ 0 - 0
connector-module-ima/build.gradle


+ 21 - 0
connector-module-ima/src/main/java/io/connector/module/ima/AFCConverter.java

@@ -0,0 +1,21 @@
+package io.connector.module.ima;
+
+import io.connector.model.afarcloud.AFCModel;
+import io.connector.model.ima.IMAModel;
+import io.vertx.core.json.JsonObject;
+
+public final class AFCConverter {
+
+    public static boolean validBySchema(JsonObject afcModel) {
+        String stringClass = afcModel.getString("__class");
+        if (stringClass == null || stringClass.isBlank()) {
+            return false;
+        }
+
+        return stringClass.equals(AFCModel.class.getName());
+    }
+
+    public static IMAModel convert(AFCModel afcModel) {
+        return new IMAModel("Converted: " + afcModel.getData());
+    }
+}

+ 18 - 0
connector-module-ima/src/main/java/io/connector/module/ima/IMAClient.java

@@ -0,0 +1,18 @@
+package io.connector.module.ima;
+
+import io.connector.model.ima.IMAModel;
+import io.vertx.core.json.JsonObject;
+
+public class IMAClient {
+
+    private final IMAConfig config;
+
+    public IMAClient(IMAConfig config) {
+        this.config = config;
+    }
+
+    public JsonObject push(IMAModel model) {
+        System.out.println(model.getData());
+        return new JsonObject().put("result", "true");
+    }
+}

+ 10 - 0
connector-module-ima/src/main/java/io/connector/module/ima/IMAConfig.java

@@ -0,0 +1,10 @@
+package io.connector.module.ima;
+
+import io.connector.core.config.DefaultConfig;
+
+public class IMAConfig {
+
+    IMAConfig(DefaultConfig defaultConfig) {
+
+    }
+}

+ 26 - 0
connector-module-ima/src/main/java/io/connector/module/ima/IMAModule.java

@@ -0,0 +1,26 @@
+package io.connector.module.ima;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleInfo;
+import io.connector.core.module.ModuleType;
+
+public class IMAModule extends AbstractModule {
+
+    private final IMAClient client;
+
+    public IMAModule(ModuleDescriptor descriptor, IMAClient client) {
+        super(descriptor, ModuleType.IMA);
+        this.client = client;
+    }
+
+    @Override
+    public void run() {
+
+    }
+
+    @Override
+    public ModuleInfo info() {
+        return new ModuleInfo(id());
+    }
+}

+ 18 - 0
connector-module-ima/src/main/java/io/connector/module/ima/IMAModuleProvider.java

@@ -0,0 +1,18 @@
+package io.connector.module.ima;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleProvider;
+
+public final class IMAModuleProvider implements ModuleProvider {
+
+    @Override
+    public AbstractModule createModule(ModuleDescriptor descriptor) {
+
+        IMAConfig config = new IMAConfig(descriptor.getServiceConfig());
+
+        IMAClient client = new IMAClient(config);
+
+        return new IMAModule(descriptor, client);
+    }
+}

+ 1 - 0
connector-module-ima/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider

@@ -0,0 +1 @@
+io.connector.module.ima.IMAModuleProvider

+ 0 - 0
connector-module-ogc-sensorthings/build.gradle


+ 14 - 0
connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsClient.java

@@ -0,0 +1,14 @@
+package io.connector.module.ogc.sensorthings;
+
+import cz.senslog.common.http.HttpClient;
+
+public class SensorThingsClient {
+
+    private final SensorThingsConfig config;
+    private final HttpClient httpClient;
+
+    public SensorThingsClient(SensorThingsConfig config, HttpClient httpClient) {
+        this.config = config;
+        this.httpClient = httpClient;
+    }
+}

+ 10 - 0
connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsConfig.java

@@ -0,0 +1,10 @@
+package io.connector.module.ogc.sensorthings;
+
+import io.connector.core.config.DefaultConfig;
+
+class SensorThingsConfig {
+
+    SensorThingsConfig(DefaultConfig defaultConfig) {
+
+    }
+}

+ 31 - 0
connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsModule.java

@@ -0,0 +1,31 @@
+package io.connector.module.ogc.sensorthings;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleInfo;
+import io.connector.module.ogc.sensorthings.gateway.SensorThingsGateway;
+import io.vertx.ext.web.Router;
+
+import static io.connector.core.module.ModuleType.OGC_SENSOR_THINGS;
+
+public class SensorThingsModule extends AbstractModule {
+
+    private final SensorThingsClient client;
+
+    SensorThingsModule(ModuleDescriptor descriptor, SensorThingsClient client) {
+        super(descriptor, OGC_SENSOR_THINGS);
+        this.client = client;
+    }
+
+    @Override
+    public void run() throws Exception {
+        registerGateway(
+                new SensorThingsGateway(vertx.eventBus(), Router.router(vertx), type, client)
+        );
+    }
+
+    @Override
+    public ModuleInfo info() {
+        return new ModuleInfo(id());
+    }
+}

+ 21 - 0
connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/SensorThingsModuleProvider.java

@@ -0,0 +1,21 @@
+package io.connector.module.ogc.sensorthings;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleProvider;
+
+import static cz.senslog.common.http.HttpClient.newHttpClient;
+
+public class SensorThingsModuleProvider implements ModuleProvider {
+
+
+    @Override
+    public AbstractModule createModule(ModuleDescriptor descriptor) {
+
+        SensorThingsConfig config = new SensorThingsConfig(descriptor.getServiceConfig());
+        SensorThingsClient client = new SensorThingsClient(config, newHttpClient());
+        SensorThingsModule module = new SensorThingsModule(descriptor, client);
+
+        return module;
+    }
+}

+ 50 - 0
connector-module-ogc-sensorthings/src/main/java/io/connector/module/ogc/sensorthings/gateway/SensorThingsGateway.java

@@ -0,0 +1,50 @@
+package io.connector.module.ogc.sensorthings.gateway;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.module.ModuleType;
+import io.connector.module.ogc.sensorthings.SensorThingsClient;
+import io.vertx.core.MultiMap;
+import io.vertx.core.eventbus.DeliveryOptions;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+
+import static io.connector.core.AddressPath.Creator.create;
+import static io.connector.core.Handler.replyToHttpContext;
+
+public class SensorThingsGateway extends AbstractGateway {
+
+    private final SensorThingsClient client;
+
+    public SensorThingsGateway(EventBus eventBus, Router router, ModuleType moduleType, SensorThingsClient client) {
+        super(eventBus, router, moduleType, ModuleType.OGC_SENSOR_THINGS);
+        this.client = client;
+    }
+
+    @Override
+    protected void run() {
+
+        router().get(create("Things(:id)")).handler(ctx -> {
+
+            DeliveryOptions options = new DeliveryOptions()
+                    .addHeader("id", ctx.pathParam("id"));
+
+            request("things", ctx.getBody(), options, replyToHttpContext(ctx));
+        });
+
+        publicConsumer("things", message -> {
+            MultiMap headers = message.headers();
+            String id = headers.get("id");
+            message.reply(new JsonObject().put("id", id));
+        });
+
+        privateConsumer("location", message -> {
+
+        });
+
+        publicConsumer("test", message -> {
+            message.reply("OK");
+        });
+
+    }
+}

+ 1 - 0
connector-module-ogc-sensorthings/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider

@@ -0,0 +1 @@
+io.connector.module.ogc.sensorthings.SensorThingsModuleProvider

+ 0 - 0
connector-module-senslog1/build.gradle


+ 386 - 0
connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1Client.java

@@ -0,0 +1,386 @@
+package io.connector.module.senslog1;
+
+import cz.senslog.common.http.HttpClient;
+import cz.senslog.common.http.HttpRequest;
+import cz.senslog.common.http.HttpResponse;
+import cz.senslog.common.http.URLBuilder;
+import io.connector.core.config.HostConfig;
+import io.connector.model.senslog1.*;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static cz.senslog.common.http.HttpContentType.TEXT_PLAIN;
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class SensLog1Client {
+
+    private static final Logger logger = LogManager.getLogger(SensLog1Client.class);
+
+    private static final DateTimeFormatter FORMATTER = ofPattern("yyyy-MM-dd HH:mm:ssZ");
+
+    private Map<Long, UnitInfo> unitInfoList;
+
+    private final SensLog1Config config;
+    private final HttpClient httpClient;
+
+    SensLog1Client(SensLog1Config config, HttpClient httpClient) {
+        this.config = config;
+        this.httpClient = httpClient;
+    }
+
+    private Map<Long, UnitInfo> getUnitInfos() {
+        if (unitInfoList == null || unitInfoList.isEmpty()) {
+            units();
+        }
+
+//        {
+//            Map<Long, UnitInfo> infos = new HashMap<>(1);
+//            for (Map.Entry<Long, UnitInfo> unitEntry : unitInfoList.entrySet()) {
+//                for (SensorInfo sensor : unitEntry.getValue().getSensors()) {
+//                    List<SensorInfo> sensors = new ArrayList<>();
+//                    sensors.add(sensor);
+//                    unitEntry.getValue().setSensors(sensors);
+//                    break;
+//                }
+//                infos.put(unitEntry.getKey(), unitEntry.getValue());
+//                break;
+//            }
+//            unitInfoList = infos;
+//        }
+
+        return unitInfoList;
+    }
+
+    public List<UnitData> lastObservations() {
+
+        HostConfig host = config.getSensorServiceHost();
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                        .addParam("Operation", "GetLastObservations")
+                        .addParam("user", config.getUser())
+                        .addParam("group", config.getGroup())
+                        .build())
+                .build();
+        HttpResponse response = httpClient.send(request);
+
+        if (response.isOk()) {
+            JsonArray jsonArray = new JsonArray(response.getBody());
+            Map<Long, UnitData> units = new HashMap<>(1);
+            for (Object jsonObject : jsonArray) {
+                if (jsonObject instanceof JsonObject) {
+                    JsonObject jsonObservations = (JsonObject)jsonObject;
+                    long unitId = jsonObservations.getLong("unitId");
+                    long sensorId = jsonObservations.getLong("sensorId");
+                    SensorObservation sensor = units.computeIfAbsent(unitId, UnitData::new).getSensor(sensorId);
+                    if (sensor == null) {
+                        sensor = new SensorObservation(sensorId);
+                        units.get(unitId).addSensor(sensor);
+                    }
+
+                    Observation observation = Observation.parse(jsonObservations);
+                    if (observation != null) {
+                        sensor.addObservation(observation);
+                    }
+                }
+            }
+
+            return new ArrayList<>(units.values());
+
+        }
+
+        return Collections.emptyList();
+    }
+
+    public void uploadPositions(List<UnitData> unitData) {
+        HostConfig host = config.getFeederServiceHost();
+
+        for (UnitData unit : unitData) {
+            for (Position position : unit.getPositions()) {
+
+                HttpRequest request = HttpRequest.newBuilder()
+                        .contentType(TEXT_PLAIN)
+                        .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                                .addParam("Operation", "InsertPosition")
+                                .addParam("date", position.getTimestamp().format(FORMATTER))
+                                .addParam("unit_id", unit.getId())
+                                .addParam("lat", position.getLatitude())
+                                .addParam("lon", position.getLongitude())
+                                .addParam("alt", position.getAltitude())
+                                .addParam("speed", position.getSpeed())
+                                .addParam("dop", position.getDilutionOfPrecision())
+                                .build())
+                        .GET().build();
+
+                HttpResponse response = httpClient.send(request);
+
+                if (response.isOk()) {
+                    logger.debug("Parsing body of the response.");
+                    boolean result = Boolean.parseBoolean(response.getBody());
+
+                    if (!result) {
+                        logger.warn("Position {} was rejected.", position);
+                    }
+                } else {
+                    logger.warn("Position {} was rejected.", position);
+                }
+            }
+        }
+    }
+
+    public List<UnitData> uploadObservations(List<UnitData> unitData) {
+
+        HostConfig host = config.getFeederServiceHost();
+
+        Map<Long, UnitData> badObservations = new HashMap<>();
+        for (UnitData unit : unitData) {
+            for (SensorObservation sensor : unit.getSensors()) {
+                for (Observation observation : sensor.getObservations()) {
+
+
+                    HttpRequest request = HttpRequest.newBuilder()
+                            .contentType(TEXT_PLAIN)
+                            .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                                    .addParam("Operation", "InsertObservation")
+                                    .addParam("value", observation.getValue())
+                                    .addParam("date", observation.getTimestamp().format(FORMATTER))
+                                    .addParam("unit_id", unit.getId())
+                                    .addParam("sensor_id", sensor.getId())
+                                    .build())
+                            .GET().build();
+
+                    /*
+                    HttpResponse response = httpClient.send(request);
+
+                    if (response.isOk()) {
+                        logger.debug("Parsing body of the response.");
+                        boolean result = Boolean.parseBoolean(response.getBody());
+
+                        if (!result) {
+                            logger.warn("Observation {} was rejected.", observation);
+                        }
+                    } else {
+
+                        SensorObservation sensorObservation = badObservations
+                                .computeIfAbsent(unit.getId(), UnitData::new)
+                                .getSensor(sensor.getId());
+
+                        if (sensorObservation == null) {
+                            sensorObservation = new SensorObservation(sensor.getId());
+                            badObservations.get(unit.getId()).addSensor(sensorObservation);
+                        }
+
+                        sensorObservation.addObservation(observation);
+
+                        logger.warn("Observation {} was rejected.", observation);
+                    }
+
+                     */
+                }
+            }
+        }
+
+        return new ArrayList<>(badObservations.values());
+    }
+
+    public List<UnitData> positions(OffsetDateTime fromDate, OffsetDateTime toDate) {
+        List<UnitData> units = new ArrayList<>();
+        for (UnitInfo unit : getUnitInfos().values()) {
+            units.addAll(positions(unit.getId(), fromDate, toDate));
+        }
+        return units;
+    }
+
+    public List<UnitData> positions(long unitId, OffsetDateTime fromDate, OffsetDateTime toDate) {
+        UnitData unitData = new UnitData(unitId);
+        positions(unitData, fromDate, toDate);
+        return Collections.singletonList(unitData);
+    }
+
+    private void positions(UnitData unit, OffsetDateTime fromDate, OffsetDateTime toDate) {
+        HostConfig host = config.getDataServiceHost();
+        logger.info("Getting observations from {}.", host.getDomain());
+
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                        .addParam("Operation", "GetPositionsDay")
+                        .addParam("user", config.getUser())
+                        .addParam("unit_id", unit.getId())
+                        .addParam("fromTime", fromDate.format(FORMATTER))
+                        .addParam("toTime", toDate.format(FORMATTER))
+                        .build())
+                .build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isOk()) {
+            JsonArray json = new JsonArray(response.getBody());
+            for (Object objJson : json) {
+                if (objJson instanceof JsonObject) {
+                    Position position = Position.parse((JsonObject)objJson);
+
+                    if (position != null) {
+                        unit.addPosition(position);
+                    }
+                }
+            }
+        }
+    }
+
+    public List<UnitData> observations(OffsetDateTime fromDate, OffsetDateTime toDate) {
+        List<UnitData> units = new ArrayList<>();
+        for (UnitInfo unit : getUnitInfos().values()) {
+            units.addAll(observations(unit.getId(), fromDate, toDate));
+        }
+        return units;
+    }
+
+    public List<UnitData> observations(long unitId, OffsetDateTime fromDate, OffsetDateTime toDate) {
+        UnitInfo unitInfo = getUnitInfos().get(unitId);
+        if (unitInfo != null) {
+            UnitData unit = new UnitData(unitInfo.getId());
+            for (SensorInfo sensorInfo : unitInfo.getSensors()) {
+                SensorObservation sensor = new SensorObservation(sensorInfo.getId());
+                unit.addSensor(sensor);
+                observations(unit, sensor, fromDate, toDate);
+            }
+            return Arrays.asList(unit);
+        }
+        return Collections.emptyList();
+    }
+
+    public List<UnitData> observations(long unitId, long sensorId, OffsetDateTime fromDate, OffsetDateTime toDate) {
+        UnitData unit = new UnitData(unitId);
+        SensorObservation sensor = new SensorObservation(sensorId);
+        unit.addSensor(sensor);
+        observations(unit, sensor, fromDate, toDate);
+        return Arrays.asList(unit);
+    }
+
+    private void observations(UnitData unit, SensorObservation sensor, OffsetDateTime fromDate, OffsetDateTime toDate) {
+
+        HostConfig host = config.getSensorServiceHost();
+        logger.info("Getting observations from {}.", host.getDomain());
+
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                        .addParam("Operation", "GetObservations")
+                        .addParam("user", config.getUser())
+                        .addParam("unit_id", unit.getId())
+                        .addParam("sensor_id", sensor.getId())
+                        .addParam("from", fromDate.format(FORMATTER))
+                        .addParam("to", toDate.format(FORMATTER))
+                        .build())
+                .build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isOk()) {
+
+            JsonArray json = new JsonArray(response.getBody());
+            for (Object obsJson : json) {
+                if (obsJson instanceof JsonObject) {
+                    Observation observation = Observation.parse((JsonObject)obsJson);
+
+                    if (observation != null) {
+                        sensor.addObservation(observation);
+                    }
+                }
+            }
+        }
+    }
+
+    public List<UnitInfo> units() {
+        HostConfig host = config.getDataServiceHost();
+        logger.info("Getting last observations from {}.", host.getDomain());
+
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                        .addParam("Operation", "GetUnitsList")
+                        .addParam("user", config.getUser())
+                        .build())
+                .build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isOk()) {
+            logger.debug("Parsing body of the response to the list of class {}.", UnitInfo.class);
+            JsonArray json = new JsonArray(response.getBody());
+            List<UnitInfo> units = new ArrayList<>(json.size());
+            unitInfoList = new HashMap<>(json.size());
+            for (Object unitJson : json) {
+                if (unitJson instanceof JsonObject) {
+                    UnitInfo unit = UnitInfo.parse((JsonObject)unitJson);
+                    if (unit != null) {
+                        units.add(unit);
+                        unitInfoList.put(unit.getId(), unit);
+                    }
+                }
+            }
+
+            for (UnitInfo unit : units) {
+                List<SensorInfo> sensors = sensors(unit.getId());
+                unit.setSensors(sensors);
+            }
+
+            return units;
+
+        } else {
+            logger.error("Can not get data from the server {}. Error {} {}",
+                    host.getDomain(), response.getStatus(), response.getBody());
+        }
+
+        return Collections.emptyList();
+    }
+
+    public List<SensorInfo> sensors(long unitId) {
+        HostConfig host = config.getSensorServiceHost();
+        logger.info("Getting last observations from {}.", host.getDomain());
+
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                        .addParam("Operation", "GetSensors")
+                        .addParam("user", config.getUser())
+                        .addParam("unit_id", unitId)
+                        .build())
+                .build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isOk()) {
+            logger.debug("Parsing body of the response to the list of class {}.", SensorInfo.class);
+            JsonArray json = new JsonArray(response.getBody());
+            List<SensorInfo> sensors = new ArrayList<>(json.size());
+            for (Object sensorJson : json) {
+                if (sensorJson instanceof JsonObject) {
+                    SensorInfo sensor = SensorInfo.parse((JsonObject)sensorJson);
+                    if (sensor != null) {
+                        sensors.add(sensor);
+                    }
+                }
+            }
+
+            logger.info("For the unit {} was added {} sensors.", unitId, sensors.size());
+
+            return sensors;
+        } else {
+            logger.error("Can not get data from the server {}. Error {} {}",
+                    host.getDomain(), response.getStatus(), response.getBody());
+
+            return Collections.emptyList();
+        }
+    }
+}

+ 43 - 0
connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1Config.java

@@ -0,0 +1,43 @@
+package io.connector.module.senslog1;
+
+import io.connector.core.config.DefaultConfig;
+import io.connector.core.config.HostConfig;
+
+class SensLog1Config {
+
+    private final HostConfig sensorServiceHost;
+    private final HostConfig dataServiceHost;
+    private final HostConfig feederServiceHost;
+
+    private final String user;
+    private final String group;
+
+    SensLog1Config(DefaultConfig config) {
+        this.sensorServiceHost = new HostConfig(config.getPropertyConfig("sensorServiceHost"));
+        this.dataServiceHost = new HostConfig(config.getPropertyConfig("dataServiceHost"));
+        this.feederServiceHost = new HostConfig(config.getPropertyConfig("feederServiceHost"));
+
+        this.user = config.getStringProperty("user");
+        this.group = config.getStringProperty("group");
+    }
+
+    public HostConfig getSensorServiceHost() {
+        return sensorServiceHost;
+    }
+
+    public HostConfig getDataServiceHost() {
+        return dataServiceHost;
+    }
+
+    public HostConfig getFeederServiceHost() {
+        return feederServiceHost;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+}

+ 50 - 0
connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1Module.java

@@ -0,0 +1,50 @@
+package io.connector.module.senslog1;
+
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.config.SchedulerConfig;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleInfo;
+import io.connector.core.module.ModuleType;
+import io.connector.module.senslog1.gateway.AFCGateway;
+import io.connector.module.senslog1.gateway.SensLog1Gateway;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+
+public class SensLog1Module extends AbstractModule {
+
+    private final SensLog1Client client;
+
+    protected SensLog1Module(ModuleDescriptor descriptor, SensLog1Client client) {
+        super(descriptor, ModuleType.SENSLOG1);
+        this.client = client;
+    }
+
+    @Override
+    public void run() throws Exception {
+        registerGateway(new AFCGateway(vertx.eventBus(), Router.router(vertx), type, client));
+        registerGateway(new SensLog1Gateway(vertx.eventBus(), Router.router(vertx), type, client));
+    }
+
+    @Override
+    public ModuleInfo info() {
+        JsonObject jsonInfo = new JsonObject()
+                .put("id", id());
+
+
+        jsonInfo.put("gateways", new JsonObject());
+
+        if (descriptor().getSchedulerConfig() != null) {
+            SchedulerConfig config = descriptor().getSchedulerConfig();
+            jsonInfo.put("scheduling", new JsonObject()
+                    .put("period", config.getPeriodSecond())
+                    .put("consumer", config.getConsumer())
+                    .put("config", new JsonObject())
+            );
+        }
+
+        jsonInfo.put("config", new JsonObject());
+
+        return new ModuleInfo(id());
+    }
+}

+ 19 - 0
connector-module-senslog1/src/main/java/io/connector/module/senslog1/SensLog1ModuleProvider.java

@@ -0,0 +1,19 @@
+package io.connector.module.senslog1;
+
+import cz.senslog.common.http.HttpClient;
+import io.connector.core.ModuleDescriptor;
+import io.connector.core.module.AbstractModule;
+import io.connector.core.module.ModuleProvider;
+
+public final class SensLog1ModuleProvider implements ModuleProvider {
+
+    @Override
+    public AbstractModule createModule(ModuleDescriptor descriptor) {
+
+        SensLog1Config config = new SensLog1Config(descriptor.getServiceConfig());
+        SensLog1Client client = new SensLog1Client(config, HttpClient.newHttpClient());
+        SensLog1Module module = new SensLog1Module(descriptor, client);
+
+        return module;
+    }
+}

+ 24 - 0
connector-module-senslog1/src/main/java/io/connector/module/senslog1/gateway/AFCGateway.java

@@ -0,0 +1,24 @@
+package io.connector.module.senslog1.gateway;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.module.ModuleType;
+import io.connector.module.senslog1.SensLog1Client;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.ext.web.Router;
+
+
+import static io.connector.core.module.ModuleType.AFARCLOUD;
+
+public class AFCGateway extends AbstractGateway {
+
+    private final SensLog1Client client;
+
+    public AFCGateway(EventBus eventBus, Router router, ModuleType moduleType, SensLog1Client client) {
+        super(eventBus, router, moduleType, AFARCLOUD);
+        this.client = client;
+    }
+
+    @Override
+    public void run() {
+    }
+}

+ 216 - 0
connector-module-senslog1/src/main/java/io/connector/module/senslog1/gateway/SensLog1Gateway.java

@@ -0,0 +1,216 @@
+package io.connector.module.senslog1.gateway;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.DataCollection;
+import io.connector.core.MessageHeader;
+import io.connector.core.module.ModuleType;
+import io.connector.model.senslog1.Observation;
+import io.connector.model.senslog1.SensorObservation;
+import io.connector.model.senslog1.UnitData;
+import io.connector.model.senslog1.UnitInfo;
+import io.connector.module.senslog1.SensLog1Client;
+import io.vertx.core.MultiMap;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+
+import java.time.OffsetDateTime;
+import java.util.*;
+
+import static io.connector.core.module.ModuleType.SENSLOG1;
+import static java.time.OffsetDateTime.MAX;
+import static java.time.OffsetDateTime.MIN;
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+public class SensLog1Gateway extends AbstractGateway {
+
+    private final SensLog1Client client;
+
+    public SensLog1Gateway(EventBus eventBus, Router router, ModuleType moduleType, SensLog1Client client) {
+        super(eventBus, router, moduleType, SENSLOG1);
+        this.client = client;
+    }
+
+    @Override
+    public void run() {
+
+        // TODO rename to a general name
+        schedulerMapping()
+                .addMapping("schedule-observations", "observations");
+
+        privateConsumer("schedule-observations", message -> {
+            List<UnitData> unitData = client.lastObservations();
+            OffsetDateTime from = MAX, to = MIN;
+            for (UnitData unit : unitData) {
+                for (SensorObservation sensor : unit.getSensors()) {
+                    for (Observation observation : sensor.getObservations()) {
+                        if (observation.getTimestamp().isBefore(from)) {
+                            from = observation.getTimestamp();
+                        }
+                        if (observation.getTimestamp().isAfter(to)) {
+                            to = observation.getTimestamp();
+                        }
+                    }
+                }
+            }
+
+            // TODO add scheduler config to the body
+
+            message.reply(message.body()).options()
+                    .addHeader("fromDate", from.format(ISO_OFFSET_DATE_TIME))
+                    .addHeader("toDate", from.plusMinutes(30).format(ISO_OFFSET_DATE_TIME));
+        });
+
+        publicConsumer("units", message -> {
+            List<UnitInfo> units = client.units();
+            message.reply(new DataCollection<>(units));
+        });
+
+        publicConsumer("add-observations", message -> {
+            String resource = message.headers().get(MessageHeader.RESOURCE);
+
+            Iterator<Object> dataIterator;
+            switch (resource) {
+                case MessageHeader.Resource.HTTP_SERVER: {
+                    dataIterator = (((Buffer)message.body()).toJsonArray()).iterator();
+                } break;
+                case MessageHeader.Resource.SCHEDULER: {
+                    dataIterator = ((DataCollection<?>)message.body()).iterator();
+                } break;
+                default: {
+                    message.fail(400, "Unknown resource of data."); return;
+                }
+            }
+
+            List<UnitData> unitData = new ArrayList<>();
+            for (Object data = dataIterator.next(); dataIterator.hasNext();) {
+                if (data instanceof JsonObject) {
+                    unitData.add(UnitData.parse((JsonObject)data));
+                } else if (data instanceof UnitData) {
+                    unitData.add((UnitData)data);
+                }
+            }
+
+            List<UnitData> unsentObservations = client.uploadObservations(unitData);
+            message.reply(new DataCollection<>(unsentObservations));
+        });
+
+        publicConsumer("unit-data", message -> {
+            MultiMap params = message.headers();
+
+            OffsetDateTime fromDate;
+            if (params.contains("fromDate")) {
+                fromDate = OffsetDateTime.parse(params.get("fromDate"), ISO_OFFSET_DATE_TIME);
+            } else {
+                message.fail(400, "Attribute 'fromDate' is required."); return;
+            }
+
+            OffsetDateTime toDate;
+            if (params.contains("toDate")) {
+                toDate = OffsetDateTime.parse(params.get("toDate"), ISO_OFFSET_DATE_TIME);
+            } else {
+                message.fail(400, "Attribute 'toDate' is required."); return;
+            }
+
+            List<UnitData> positions, observations;
+            if (params.contains("unitId")) {
+                long unitId = Long.parseLong(params.get("unitId"));
+                positions = client.positions(unitId, fromDate, toDate);
+                observations = client.observations(unitId, fromDate, toDate);
+            } else {
+                positions = client.positions(fromDate, toDate);
+                observations = client.observations(fromDate, toDate);
+            }
+
+            List<UnitData> mergedUnitData = mergeUnitDate(positions, observations);
+            message.reply(new DataCollection<>(mergedUnitData));
+        });
+
+        publicConsumer("positions", message -> {
+            MultiMap params = message.headers();
+
+            OffsetDateTime fromDate;
+            if (params.contains("fromDate")) {
+                fromDate = OffsetDateTime.parse(params.get("fromDate"), ISO_OFFSET_DATE_TIME);
+            } else {
+                message.fail(400, "Attribute 'fromDate' is required."); return;
+            }
+
+            OffsetDateTime toDate;
+            if (params.contains("toDate")) {
+                toDate = OffsetDateTime.parse(params.get("toDate"), ISO_OFFSET_DATE_TIME);
+            } else {
+                message.fail(400, "Attribute 'toDate' is required."); return;
+            }
+
+            List<UnitData> positions;
+            if (params.contains("unitId")) {
+                long unitId = Long.parseLong(params.get("unitId"));
+                positions = client.positions(unitId, fromDate, toDate);
+            } else {
+                positions = client.positions(fromDate, toDate);
+            }
+
+            message.reply(new DataCollection<>(positions));
+        });
+
+        publicConsumer("observations", message -> {
+            // TODO get filter/config from message.body()
+
+            MultiMap params = message.headers();
+
+            OffsetDateTime fromDate;
+            if (params.contains("fromDate")) {
+                fromDate = OffsetDateTime.parse(params.get("fromDate"), ISO_OFFSET_DATE_TIME);
+            } else {
+                message.fail(400, "Attribute 'fromDate' is required."); return;
+            }
+
+            OffsetDateTime toDate;
+            if (params.contains("toDate")) {
+                toDate = OffsetDateTime.parse(params.get("toDate"), ISO_OFFSET_DATE_TIME);
+            } else {
+                message.fail(400, "Attribute 'toDate' is required."); return;
+            }
+
+            List<UnitData> observations;
+            if (params.contains("unitId")) {
+                long unitId = Long.parseLong(params.get("unitId"));
+                if (params.contains("sensorId")) {
+                    long sensorId = Long.parseLong(params.get("sensorId"));
+                    observations = client.observations(unitId, sensorId, fromDate, toDate);
+                } else {
+                    observations = client.observations(unitId, fromDate, toDate);
+                }
+            } else {
+                observations = client.observations(fromDate, toDate);
+            }
+
+            message.reply(new DataCollection<>(observations));
+        });
+    }
+
+    @SafeVarargs
+    private static List<UnitData> mergeUnitDate(List<UnitData> ...unitsToMerge) {
+        int len = unitsToMerge.length;
+        Map<Long, List<UnitData>> units = new HashMap<>();
+        for (List<UnitData> unitDataList : unitsToMerge) {
+            for (UnitData unitData : unitDataList) {
+                units.computeIfAbsent(unitData.getId(), k -> new ArrayList<>(len))
+                        .add(unitData);
+            }
+        }
+        List<UnitData> result = new ArrayList<>();
+
+        for (Map.Entry<Long, List<UnitData>> unitDataEntry : units.entrySet()) {
+            UnitData unit = new UnitData(unitDataEntry.getKey());
+            for (UnitData unitData : unitDataEntry.getValue()) {
+                unit.mergeIn(unitData);
+            }
+            result.add(unit);
+        }
+
+        return result;
+    }
+}

+ 1 - 0
connector-module-senslog1/src/main/resources/META-INF/services/io.connector.core.module.ModuleProvider

@@ -0,0 +1 @@
+io.connector.module.senslog1.SensLog1ModuleProvider

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Tue Jul 07 11:00:55 CEST 2020
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 9 - 0
settings.gradle

@@ -0,0 +1,9 @@
+rootProject.name = 'connector'
+include 'connector-core'
+include 'connector-model'
+include 'connector-module-afarcloud'
+include 'connector-module-ima'
+include 'connector-app'
+include 'connector-module-senslog1'
+include 'connector-module-ogc-sensorthings'
+

+ 10 - 0
src/main/java/io/connector/Main.java

@@ -0,0 +1,10 @@
+package io.connector;
+
+import io.connector.app.Application;
+
+public class Main {
+
+    public static void main(String[] args) throws Exception {
+        Application.init(args).start();
+    }
+}