浏览代码

Architecture of connectors, created connector from Azure to Senslog1

Lukas Cerny 6 年之前
当前提交
461b1ad0af
共有 100 个文件被更改,包括 6388 次插入0 次删除
  1. 二进制
      .gitignore
  2. 39 0
      config/default.yaml
  3. 52 0
      connector-app/pom.xml
  4. 94 0
      connector-app/src/main/java/cz/senslog/connector/app/Application.java
  5. 15 0
      connector-app/src/main/java/cz/senslog/connector/app/Main.java
  6. 95 0
      connector-app/src/main/java/cz/senslog/connector/app/config/AppConfig.java
  7. 85 0
      connector-app/src/main/java/cz/senslog/connector/app/config/Connector.java
  8. 221 0
      connector-app/src/main/java/cz/senslog/connector/app/config/ConnectorBuilder.java
  9. 61 0
      connector-app/src/main/java/cz/senslog/connector/app/config/Parameters.java
  10. 53 0
      connector-app/src/main/java/cz/senslog/connector/app/config/ServiceProvider.java
  11. 2 0
      connector-app/src/main/resources/project.properties
  12. 257 0
      connector-app/src/test/java/ConnectorBuilderTest.java
  13. 43 0
      connector-app/src/test/java/cz/senslog/connector/app/config/ParametersTest.java
  14. 66 0
      connector-common/pom.xml
  15. 49 0
      connector-common/src/main/java/cz/senslog/connector/config/ConfigurationServiceImpl.java
  16. 46 0
      connector-common/src/main/java/cz/senslog/connector/config/DatabaseBuilderImpl.java
  17. 47 0
      connector-common/src/main/java/cz/senslog/connector/config/DatabaseConfigurationServiceImpl.java
  18. 42 0
      connector-common/src/main/java/cz/senslog/connector/config/FileBuilderImpl.java
  19. 263 0
      connector-common/src/main/java/cz/senslog/connector/config/FileConfigurationServiceImpl.java
  20. 51 0
      connector-common/src/main/java/cz/senslog/connector/config/api/ConfigurationService.java
  21. 40 0
      connector-common/src/main/java/cz/senslog/connector/config/api/DatabaseBuilder.java
  22. 20 0
      connector-common/src/main/java/cz/senslog/connector/config/api/DatabaseConfigurationService.java
  23. 25 0
      connector-common/src/main/java/cz/senslog/connector/config/api/FileBuilder.java
  24. 22 0
      connector-common/src/main/java/cz/senslog/connector/config/api/FileConfigurationService.java
  25. 61 0
      connector-common/src/main/java/cz/senslog/connector/config/model/ConnectorDescriptor.java
  26. 42 0
      connector-common/src/main/java/cz/senslog/connector/config/model/DefaultConfig.java
  27. 52 0
      connector-common/src/main/java/cz/senslog/connector/config/model/HostConfig.java
  28. 135 0
      connector-common/src/main/java/cz/senslog/connector/config/model/PropertyConfig.java
  29. 8 0
      connector-common/src/main/java/cz/senslog/connector/exception/PropertyNotFoundException.java
  30. 8 0
      connector-common/src/main/java/cz/senslog/connector/exception/SyntaxException.java
  31. 10 0
      connector-common/src/main/java/cz/senslog/connector/exception/UnsupportedFileException.java
  32. 8 0
      connector-common/src/main/java/cz/senslog/connector/http/ContentType.java
  33. 182 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpClient.java
  34. 20 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpCode.java
  35. 6 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpHeader.java
  36. 5 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpMethod.java
  37. 101 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpRequest.java
  38. 69 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpRequestBuilder.java
  39. 77 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpResponse.java
  40. 42 0
      connector-common/src/main/java/cz/senslog/connector/http/HttpResponseBuilder.java
  41. 113 0
      connector-common/src/main/java/cz/senslog/connector/http/URLBuilder.java
  42. 185 0
      connector-common/src/main/java/cz/senslog/connector/json/BasicJson.java
  43. 122 0
      connector-common/src/main/java/cz/senslog/connector/json/JsonSchema.java
  44. 30 0
      connector-common/src/main/java/cz/senslog/connector/util/ClassUtils.java
  45. 31 0
      connector-common/src/main/java/cz/senslog/connector/util/Next.java
  46. 33 0
      connector-common/src/main/java/cz/senslog/connector/util/NextImpl.java
  47. 25 0
      connector-common/src/main/java/cz/senslog/connector/util/NumberUtils.java
  48. 20 0
      connector-common/src/main/java/cz/senslog/connector/util/Pipe.java
  49. 27 0
      connector-common/src/main/java/cz/senslog/connector/util/PipeImpl.java
  50. 38 0
      connector-common/src/main/java/cz/senslog/connector/util/Pipeline.java
  51. 60 0
      connector-common/src/main/java/cz/senslog/connector/util/StringUtils.java
  52. 63 0
      connector-common/src/main/java/cz/senslog/connector/util/Triple.java
  53. 51 0
      connector-common/src/main/java/cz/senslog/connector/util/Tuple.java
  54. 20 0
      connector-common/src/main/resources/log4j2.xml
  55. 18 0
      connector-common/src/test/java/cz/senslog/connector/config/FileBuilderImplTest.java
  56. 67 0
      connector-common/src/test/java/cz/senslog/connector/config/FileConfigurationServiceImplTest.java
  57. 4 0
      connector-common/src/test/java/cz/senslog/connector/config/TestFetchProviderClass.java
  58. 4 0
      connector-common/src/test/java/cz/senslog/connector/config/TestPushProviderClass.java
  59. 18 0
      connector-common/src/test/java/cz/senslog/connector/config/api/DefaultConfigTest.java
  60. 21 0
      connector-common/src/test/java/cz/senslog/connector/config/model/ConnectorDescriptorTest.java
  61. 19 0
      connector-common/src/test/java/cz/senslog/connector/config/model/HostConfigTest.java
  62. 66 0
      connector-common/src/test/java/cz/senslog/connector/config/model/PropertyConfigTest.java
  63. 111 0
      connector-common/src/test/java/cz/senslog/connector/http/URLBuilderTest.java
  64. 26 0
      connector-common/src/test/java/cz/senslog/connector/util/ClassUtilsTest.java
  65. 28 0
      connector-common/src/test/java/cz/senslog/connector/util/NumberUtilsTest.java
  66. 29 0
      connector-common/src/test/java/cz/senslog/connector/util/PipelineTest.java
  67. 36 0
      connector-common/src/test/java/cz/senslog/connector/util/StringUtilsTest.java
  68. 18 0
      connector-common/src/test/java/cz/senslog/connector/util/TripleTest.java
  69. 17 0
      connector-common/src/test/java/cz/senslog/connector/util/TupleTest.java
  70. 14 0
      connector-common/src/test/resources/test_valid_config.yaml
  71. 38 0
      connector-fetch-api/pom.xml
  72. 61 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/ConnectorFetch.java
  73. 21 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetchProvider.java
  74. 27 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetcher.java
  75. 37 0
      connector-fetch-azure/pom.xml
  76. 63 0
      connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/AzureConfig.java
  77. 248 0
      connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/AzureFetcher.java
  78. 46 0
      connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/ConnectorFetchAzureProvider.java
  79. 50 0
      connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/auth/AzureAuthConfig.java
  80. 167 0
      connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/auth/AzureAuthenticationService.java
  81. 59 0
      connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/auth/AzureAuthorizationInfo.java
  82. 1 0
      connector-fetch-azure/src/main/resources/META-INF/services/cz.senslog.connector.fetch.api.ConnectorFetchProvider
  83. 76 0
      connector-fetch-azure/src/main/resources/schema/sensorDataSchema.json
  84. 106 0
      connector-fetch-azure/src/main/resources/schema/sensorInfoSchema.json
  85. 101 0
      connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/AzureConfigTest.java
  86. 279 0
      connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/AzureFetcherTest.java
  87. 46 0
      connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/ConnectorFetchAzureProviderTest.java
  88. 281 0
      connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/auth/AzureAuthenticationServiceTest.java
  89. 83 0
      connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/auth/AzureAuthorizationInfoTest.java
  90. 76 0
      connector-fetch-azure/src/test/resources/schema/sensorDataSchema.json
  91. 106 0
      connector-fetch-azure/src/test/resources/schema/sensorInfoSchema.json
  92. 22 0
      connector-model/pom.xml
  93. 30 0
      connector-model/src/main/java/cz/senslog/connector/model/api/AbstractModel.java
  94. 24 0
      connector-model/src/main/java/cz/senslog/connector/model/api/Converter.java
  95. 63 0
      connector-model/src/main/java/cz/senslog/connector/model/api/ConverterProvider.java
  96. 35 0
      connector-model/src/main/java/cz/senslog/connector/model/azure/AzureModel.java
  97. 114 0
      connector-model/src/main/java/cz/senslog/connector/model/azure/SensorData.java
  98. 170 0
      connector-model/src/main/java/cz/senslog/connector/model/azure/SensorInfo.java
  99. 34 0
      connector-model/src/main/java/cz/senslog/connector/model/azure/SensorType.java
  100. 96 0
      connector-model/src/main/java/cz/senslog/connector/model/converter/AzureModelSenslogV1ModelConverter.java

二进制
.gitignore


+ 39 - 0
config/default.yaml

@@ -0,0 +1,39 @@
+settings:
+    - AzureLoraWan:
+        name: "IoT LoraWan"
+        provider: "cz.senslog.connector.fetch.azure.ConnectorFetchAzureProvider"
+        startDate: 2019-07-13T11:37:14.900
+        limitPerSensor: 60
+        
+        sensorInfoHost:
+            domain: "https://iotlorawan.azurewebsites.net"
+            path: "api/sensors"
+        
+        sensorDataHost:
+            domain: "https://iotlorawan.azurewebsites.net"
+            path: "api/sensordata"
+        
+        authentication:
+            host:
+                domain: "https://iotlorawan.azurewebsites.net"
+                path: "api/accounts/login"
+            username: "<username>"
+            password: "<password>"
+            refreshPeriodIfFail: 10000
+        
+    - SenslogV1:
+        name: "Senslog V1"
+        provider: "cz.senslog.connector.push.rest.senslog.v1.SenslogV1ConnectorPushProvider"
+        host:
+            domain: "http://localhost:8080"
+            path: "DBService-1.4-SNAPSHOT/FeederServlet"
+
+    - SenslogV2:
+          name: "Senslog V2"
+          provider: "cz.senslog.connector.push.rest.senslog.v2.SenslogV2ConnectorPushProvider"
+
+connectors:
+    - AzureSenslogV1:
+        fetcher: "AzureLoraWan"
+        pusher: "SenslogV1"
+        period: 60

+ 52 - 0
connector-app/pom.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>connectors</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-app</artifactId>
+
+    <version>${project.parent.version}</version>
+    <name>${project.parent.name}</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-fetch-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-push-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.beust</groupId>
+            <artifactId>jcommander</artifactId>
+            <version>1.72</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 94 - 0
connector-app/src/main/java/cz/senslog/connector/app/Application.java

@@ -0,0 +1,94 @@
+package cz.senslog.connector.app;
+
+import cz.senslog.connector.app.config.*;
+import cz.senslog.connector.config.api.ConfigurationService;
+import cz.senslog.connector.config.api.FileConfigurationService;
+import cz.senslog.connector.fetch.ConnectorFetch;
+import cz.senslog.connector.model.converter.ModelConverterProvider;
+import cz.senslog.connector.push.ConnectorPush;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * The class {@code Application} represents a trigger for entire application.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class Application implements Runnable {
+
+    private static Logger logger = LogManager.getLogger(Application.class);
+
+
+    /** Initialization delay value when the scheduler starts to schedule tasks (in seconds). */
+    private static int INITIAL_TASK_DELAY = 2;
+
+    /** Default value for scheduling tasks when the value missing in the configuration file (in seconds). */
+    private static int DEFAULT_SCHEDULE_PERIOD = 3600;  // every hour
+
+    /** Attribute of basic configuration values of the application. */
+    private final AppConfig appConfig;
+
+    /** Attribute of input parameters of the application. */
+    private final Parameters params;
+
+    /**
+     * Initialization method to trigger the application.
+     * @param args - array of parameters.
+     * @return new thread of {@code Runnable}.
+     * @throws IOException throws if input parameters or application configuration file can not be parsed.
+     */
+    static Runnable init(String... args) throws IOException {
+        AppConfig appConfig = AppConfig.load();
+        Parameters parameters = Parameters.parse(args);
+        return new Application(appConfig, parameters);
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static init method {@link Application#init(String...)}.
+     * @param appConfig basic configuration of the application. More info of the class {@see AppConfig}.
+     * @param parameters parsed input parameters of the application. More info of the class  {@see Parameters}.
+     */
+    private Application(AppConfig appConfig, Parameters parameters) {
+        this.appConfig = appConfig;
+        this.params = parameters;
+    }
+
+    @Override
+    public void run() {
+        logger.info("Starting application {} version {}", appConfig.getName(), appConfig.getVersion());
+
+        try {
+            FileConfigurationService configService = ConfigurationService.newFileBuilder()
+                    .fileName(params.getConfigFileName()).build();
+
+            configService.load();
+
+            ServiceProvider serviceProvider = new ServiceProvider(ConnectorFetch::getProvider, ConnectorPush::getProvider);
+            ModelConverterProvider connectorProvider = new ModelConverterProvider();
+
+            Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, connectorProvider, configService).createConnectors();
+            if (!connectors.isEmpty()) {
+                logger.info("Starting a scheduler for connectors.");
+                ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(connectors.size());
+                connectors.forEach(conn -> {
+                    int schedulePeriod = conn.getPeriod().orElse(DEFAULT_SCHEDULE_PERIOD);
+                    logger.info("Scheduling the {} with the period {} seconds.", conn.getName(), schedulePeriod);
+                    scheduler.scheduleAtFixedRate(conn.getTask(), INITIAL_TASK_DELAY, schedulePeriod, SECONDS);
+                });
+            } else {
+                logger.warn("No connectors were loaded.");
+            }
+        } catch (Exception e) {
+            logger.catching(e);
+        }
+    }
+}

+ 15 - 0
connector-app/src/main/java/cz/senslog/connector/app/Main.java

@@ -0,0 +1,15 @@
+package cz.senslog.connector.app;
+
+/**
+ * Main of the application.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class Main {
+
+    public static void main(String[] args) throws Exception {
+        Application.init(args).run();
+    }
+}

+ 95 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/AppConfig.java

@@ -0,0 +1,95 @@
+package cz.senslog.connector.app.config;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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
+ */
+public class AppConfig {
+
+    private static Logger logger = LogManager.getLogger(AppConfig.class);
+
+
+    /** 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 {
+        logger.debug("Loading application configuration file '{}'", PROPERTIES_FILE_NAME);
+
+        Properties properties = new Properties();
+
+        logger.debug("Getting the class loader from the class {}.", AppConfig.class.getName());
+        ClassLoader loader = AppConfig.class.getClassLoader();
+
+        logger.debug("Opening the file '{}'.", PROPERTIES_FILE_NAME);
+        InputStream stream = loader.getResourceAsStream(PROPERTIES_FILE_NAME);
+
+        logger.debug("Parsing application configuration file '{}'", PROPERTIES_FILE_NAME);
+        properties.load(stream);
+
+        logger.debug("Application configuration file was loaded successfully.");
+        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) {
+        logger.debug("Getting property with the name '{}'", propertyName);
+        String value = properties.getProperty(propertyName);
+
+        if (value == null) {
+            logger.debug("Property '{}' was not found. Used default property value '{}'.", propertyName, defaultValue);
+            return defaultValue;
+        } else {
+            logger.debug("Property '{}' was loaded successfully.", propertyName);
+            return value;
+        }
+    }
+}

+ 85 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/Connector.java

@@ -0,0 +1,85 @@
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.push.api.ConnectorPusher;
+
+import java.util.Optional;
+
+import static cz.senslog.connector.util.Pipeline.of;
+
+/**
+ * The class {@code Connector} represents a created connector
+ * which allows to be scheduled by defined period.
+ *
+ * The idea is to wrap functionality of a connector. The flow is
+ * defined as 'fetcher' -> 'converter' -> 'pusher'.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class Connector {
+
+    /** Name of the connector */
+    private final String name;
+
+    /** Instance of a fetcher that provides data. */
+    private final ConnectorFetcher<? super AbstractModel> fetcher;
+
+    /** Instance of a pusher that receives data. */
+    private final ConnectorPusher<? super AbstractModel> pusher;
+
+    /** Converter between fetch and push. */
+    private final Converter<? super AbstractModel, ? super AbstractModel> converter;
+
+    /** Period for scheduling. */
+    private final Optional<Integer> period;
+
+    /**
+     * Constructor allows to set all attributes.
+     * @param name - name of the connector.
+     * @param fetcher - instance of fetcher.
+     * @param pusher - instance of pusher.
+     * @param converter - instance of converter.
+     * @param period - period for scheduling.
+     */
+    public Connector(
+            String name,
+            ConnectorFetcher<? super AbstractModel> fetcher,
+            ConnectorPusher<? super AbstractModel> pusher,
+            Converter<? super AbstractModel, ? super AbstractModel> converter,
+            Integer period
+    ) {
+        this.name = name;
+        this.fetcher = fetcher;
+        this.pusher = pusher;
+        this.converter = converter;
+        this.period = Optional.ofNullable(period);
+    }
+
+    /**
+     * Returns name of the connector.
+     * @return name of the connector.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns period for scheduling.
+     * @return period for scheduling.
+     */
+    public Optional<Integer> getPeriod() {
+        return period;
+    }
+
+    /**
+     * Returns scheduling runnable task of the connector flow.
+     * @return runnable task
+     */
+    public Runnable getTask() {
+        return () -> of(fetcher::fetch).pipe(converter::convert).end(pusher::push);
+    }
+}

+ 221 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/ConnectorBuilder.java

@@ -0,0 +1,221 @@
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.config.api.ConfigurationService;
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.config.model.ConnectorDescriptor;
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.api.ConverterProvider;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+import cz.senslog.connector.push.api.ConnectorPusher;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
+
+import static java.lang.String.format;
+
+/**
+ * The class {@code ConnectorBuilder} provides a builder for the class {@link Connector}.
+ * The class creates new connectors according to configuration.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ConnectorBuilder {
+
+    private static Logger logger = LogManager.getLogger(ConnectorBuilder.class);
+
+    /** Attribute provides fetch/push providers ({@link ServiceProvider}). */
+    private final ServiceProvider serviceProvider;
+
+    /** Attribute provides converters ({@link ConverterProvider}). */
+    private final ConverterProvider converterProvider;
+
+    /** Attribute provides configuration ({@link ConfigurationService}). */
+    private final ConfigurationService configService;
+
+    /**
+     * Static method for initialization. Sets all attributes.
+     * @param serviceProvider - service for fetch/push providers.
+     * @param converterProvider - service for converter.
+     * @param configService - service for configuration.
+     * @return new instance of {@code ConnectorBuilder}.
+     */
+    public static ConnectorBuilder init(ServiceProvider serviceProvider, ConverterProvider converterProvider, ConfigurationService configService) {
+        return new ConnectorBuilder(serviceProvider, converterProvider, configService);
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static init method {@link ConnectorBuilder#init(ServiceProvider, ConverterProvider, ConfigurationService)}.
+     * @param serviceProvider - service for fetch/push providers.
+     * @param converterProvider - service for converter.
+     * @param configService - service for configuration.
+     */
+    private ConnectorBuilder(ServiceProvider serviceProvider, ConverterProvider converterProvider, ConfigurationService configService) {
+        this.serviceProvider = serviceProvider;
+        this.converterProvider = converterProvider;
+        this.configService = configService;
+    }
+
+    /**
+     * Creates and returns new instance of fetcher.
+     * @param fetchProviderClass - class of fetcher provider.
+     * @return new instance of fetcher {@code ConnectorFetcher}.
+     * @throws Exception throws if the fetch provider does not exist or any configuration does not exists.
+     */
+    private ConnectorFetcher getFetcherInstance(Class<?> fetchProviderClass) throws Exception {
+        logger.debug("Creating a new instance of fetcher for {}.", fetchProviderClass);
+
+        ConnectorFetchProvider provider = serviceProvider.getFetchProvider(fetchProviderClass);
+        if (provider == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a fetch provider instance for the %s.", fetchProviderClass
+            )));
+        }
+
+        DefaultConfig config = configService.getConfigForClass(provider.getClass());
+        if (config == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a default settings for the provider %s.", provider.getClass()
+            )));
+        }
+
+        return provider.createFetcher(config);
+    }
+
+    /**
+     * Creates and returns new instance of pusher.
+     * @param pushProviderClass - class of push provider.
+     * @return new instance of pusher {@code ConnectorPusher}.
+     * @throws Exception throws if the push provider does not exist or any configuration does not exists.
+     */
+    private ConnectorPusher getPusherInstance(Class<?> pushProviderClass) throws Exception {
+        logger.debug("Creating a new instance of pusher for {}.", pushProviderClass);
+
+        ConnectorPushProvider provider = serviceProvider.getPushProvider(pushProviderClass);
+        if (provider == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a push provider instance for the %s.", pushProviderClass
+            )));
+        }
+
+        DefaultConfig config = configService.getConfigForClass(provider.getClass());
+        if (config == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a default settings for the provider %s.", provider.getClass()
+            )));
+        }
+
+        return provider.createPusher(config);
+    }
+
+    /**
+     * Creates connectors depends on configuration.
+     * For each connector descriptor is loaded fetch and push provider and created fetcher and pusher.
+     * From these instances is get their input model which is child of {@link AbstractModel}.
+     * If everything is successful then is called #init() method and created a new connector {@link Connector}.
+     * If anything throws an exception, creating of the connector will be skipped.
+     * @return set of created and valid connectors.
+     */
+    public Set<Connector> createConnectors() {
+        logger.info("Starting to create new connectors.");
+
+        logger.debug("Getting all connector descriptors from the configuration service.");
+        Set<ConnectorDescriptor> connectorDescriptors = configService.getConnectorDescriptors();
+        logger.debug("Creating an empty set of connectors with init size {}.", connectorDescriptors.size());
+        Set<Connector> connectors = new HashSet<>(connectorDescriptors.size());
+
+        for (ConnectorDescriptor connDesc : connectorDescriptors) {
+            try {
+                logger.debug("Getting descriptors for a new '{}' connector connection.", connDesc.getName());
+                logger.debug("Connector: {}", connDesc);
+
+                ConnectorFetcher fetcher = getFetcherInstance(connDesc.getFetcher());
+                ConnectorPusher pusher = getPusherInstance(connDesc.getPusher());
+
+                Class<? extends AbstractModel> inputModel = getAbstractModelFromGeneric(fetcher.getClass());
+                Class<? extends AbstractModel> outputModel = getAbstractModelFromGeneric(pusher.getClass());
+
+                Converter converter = converterProvider.getConverter(inputModel, outputModel);
+                if (converter == null) {
+                    throw logger.throwing(new Exception(format(
+                            "Can not find converter for connector: %s -> %s.", fetcher.getClass(), pusher.getClass()
+                    )));
+                }
+
+                logger.info("Invocation of initialization method for the {}.", fetcher.getClass());
+                fetcher.init();
+
+                logger.info("Invocation of initialization method for the {}.", pusher.getClass());
+                pusher.init();
+
+                logger.debug("Creating a new {} connector.", connDesc.getName());
+                Connector connector = new Connector(connDesc.getName(), fetcher, pusher, converter, connDesc.getPeriod());
+
+                logger.debug("Saving the {} connector.", connDesc.getName());
+                connectors.add(connector);
+
+                logger.info("New connector connection {} was created successfully.", connDesc.getName());
+            } catch (Exception e) {
+                logger.error("Creating of the connector {} was skipped.", connDesc.getName());
+                logger.catching(e);
+            }
+        }
+        return connectors;
+    }
+
+    /**
+     * Gets a generic parameters from the input class.
+     * Input class could be type of {@code ConnectorFetcher} or {@code ConnectorPusher}.
+     * @param aClass - class contain generic parameters.
+     * @return generic parameter extended from {@link AbstractModel}.
+     * @throws Exception throws if can not be get generic parameter from the input class.
+     */
+    @SuppressWarnings("unchecked")
+    private Class<? extends AbstractModel> getAbstractModelFromGeneric(Class aClass) throws Exception {
+        Type[] classTypes = aClass.getGenericInterfaces();
+        if (classTypes.length == 0) {
+            throw logger.throwing(new Exception(format(
+                "%s does not implements any interface.", aClass
+            )));
+        }
+
+        Type interfaceType = classTypes[0];
+        if (!(interfaceType instanceof ParameterizedType)) {
+            throw logger.throwing(new Exception(format(
+                    "%s implemented interface does not contain generic parameters.", aClass
+            )));
+        }
+
+        ParameterizedType parameterizedInterfaceType = (ParameterizedType) interfaceType;
+        Type[] classArgumentTypes = parameterizedInterfaceType.getActualTypeArguments();
+        if (classArgumentTypes.length == 0) {
+            throw logger.throwing(new Exception(format(
+                    "%s implements empty generic parameters.", aClass
+            )));
+        }
+
+        Type classModelType = classArgumentTypes[0];
+        if (!(classModelType instanceof Class)) {
+            throw logger.throwing(new Exception(format(
+                    "%s contains generic parameters which are not instance of %s.", aClass, Class.class
+            )));
+        }
+
+        Class<?> classModel = (Class<?>) classModelType;
+        if (!AbstractModel.class.isAssignableFrom(classModel)) {
+            throw logger.throwing(new Exception(format(
+                    "%s does not contain generic parameters extended from %s", aClass, AbstractModel.class
+            )));
+        }
+
+       return (Class<? extends AbstractModel>) classModel;
+    }
+}

+ 61 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/Parameters.java

@@ -0,0 +1,61 @@
+package cz.senslog.connector.app.config;
+
+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.connector.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
+ */
+public final class Parameters {
+
+    private static Logger logger = LogManager.getLogger(Parameters.class);
+
+    /**
+     * Static method to parse input parameters.
+     * @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(String... args) throws IOException {
+        logger.debug("Parsing input parameters {}", Arrays.toString(args));
+
+        Parameters parameters = new Parameters();
+        JCommander.newBuilder().addObject(parameters).build().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 = {"-cf", "-config-file"}, description = "Configuration file in .yaml format.")
+    private String configFileName;
+
+    /**
+     * Returns name of the configuration file.
+     * @return string name.
+     */
+    public String getConfigFileName() {
+        return configFileName;
+    }
+}

+ 53 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/ServiceProvider.java

@@ -0,0 +1,53 @@
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+
+import java.util.function.Function;
+
+/**
+ * The class {@code ServiceProvider} represents a wrapper for
+ * fetch and push providers.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ServiceProvider {
+
+    /** Function provides fetcher instance of input class. */
+    private final Function<Class<?>, ConnectorFetchProvider> fetchProviderFnc;
+
+    /** Function provides pusher instance of input class. */
+    private final Function<Class<?>, ConnectorPushProvider> pushProviderFnc;
+
+    /**
+     * Constructor allows to set all attributes.
+     * @param fetchProviderFnc - function to provider fetch instance.
+     * @param pushProviderFnc - function to provide push instance.
+     */
+    public ServiceProvider(Function<Class<?>, ConnectorFetchProvider> fetchProviderFnc,
+                           Function<Class<?>, ConnectorPushProvider> pushProviderFnc
+    ) {
+        this.fetchProviderFnc = fetchProviderFnc;
+        this.pushProviderFnc = pushProviderFnc;
+    }
+
+    /**
+     * Returns fetch instance depends on input class.
+     * @param providerClass - class of fetch.
+     * @return instance of fetch.
+     */
+    public ConnectorFetchProvider getFetchProvider(Class<?> providerClass) {
+        return fetchProviderFnc.apply(providerClass);
+    }
+
+    /**
+     * Returns push instance depends on input class.
+     * @param providerClass - class of push.
+     * @return instance of push.
+     */
+    public ConnectorPushProvider getPushProvider(Class<?> providerClass) {
+        return pushProviderFnc.apply(providerClass);
+    }
+}

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

@@ -0,0 +1,2 @@
+app.name=${project.name}
+app.version=${project.version}

+ 257 - 0
connector-app/src/test/java/ConnectorBuilderTest.java

@@ -0,0 +1,257 @@
+import cz.senslog.connector.app.config.Connector;
+import cz.senslog.connector.app.config.ConnectorBuilder;
+import cz.senslog.connector.app.config.ServiceProvider;
+import cz.senslog.connector.config.api.ConfigurationService;
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.config.model.ConnectorDescriptor;
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.api.ConverterProvider;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+import cz.senslog.connector.push.api.ConnectorPusher;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static java.time.LocalDateTime.MAX;
+import static java.time.LocalDateTime.MIN;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ConnectorBuilderTest {
+
+    private static class InputModel extends AbstractModel{ InputModel() { super(MIN, MAX); }}
+    private static class OutputModel extends AbstractModel{ OutputModel() { super(MIN, MAX);}}
+
+    private final ConnectorFetchProvider defaultFetchProvider = config -> new ConnectorFetcher<OutputModel>() {
+        @Override public void init() {}
+        @Override public OutputModel fetch() { return new OutputModel(); }
+    };
+
+    private final ConnectorPushProvider defaultPushProvider = config -> new ConnectorPusher<InputModel>() {
+        @Override public void init() {}
+        @Override public void push(InputModel model) {}
+    };
+
+    @Test
+    void createConnectors_CreateTestConnector_CreatedOneConnector() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(1, connectors.size());
+
+        Connector connector = connectors.iterator().next();
+        assertEquals("Test", connector.getName());
+        assertEquals(1, connector.getPeriod().orElse(0));
+    }
+
+    @Test
+    void createConnectors_CreateTwoConnectors_CreatedTwoConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test1", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+        connectorDescriptors.add(new ConnectorDescriptor("Test2", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 2));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(2, connectors.size());
+    }
+
+    @Test
+    void createConnectors_FetchProviderNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        // fetch provider class does not exist -> null
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> null, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_FetchConfigNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        // fetch provider configuration does not exist -> null
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(null);
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_PushProviderNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        // push provider is set to null
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> null);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_PushConfigNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        // fetch provider configuration does not exist -> null
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(null);
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_ConverterNull_CreatedZeroConnector() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        // converter does not exist -> null
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(null);
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_IncompatibleFetchModelClass_CreatedZeroConnector() {
+
+        // ConnectorFetcher does not contain model class as a generic parameter
+         ConnectorFetchProvider fetchProvider = config -> new ConnectorFetcher() {
+            @Override public void init() {}
+            @Override public OutputModel fetch() { return new OutputModel(); }
+        };
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> fetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", fetchProvider.getClass(), defaultPushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(fetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForClass(defaultPushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_IncompatiblePushModelClass_CreatedZeroConnector() {
+
+        // ConnectorPusher does not contain model class as a generic parameter
+        ConnectorPushProvider pushProvider = config -> new ConnectorPusher() {
+            @Override public void init() {}
+            @Override public void push(AbstractModel model) {}
+        };
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> pushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", defaultFetchProvider.getClass(), pushProvider.getClass(), 1));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForClass(defaultFetchProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForClass(pushProvider.getClass())).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+}

+ 43 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/ParametersTest.java

@@ -0,0 +1,43 @@
+package cz.senslog.connector.app.config;
+
+import com.beust.jcommander.ParameterException;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ParametersTest {
+
+    @Test
+    void parse_FileParams_True() throws Exception {
+
+        File yamlConfigFile = File.createTempFile("config_test_", ".yaml");
+        String [] params = new String[] {"-cf", yamlConfigFile.getAbsolutePath()};
+
+        Parameters parameters = Parameters.parse(params);
+
+        assertEquals(yamlConfigFile.getAbsolutePath(), parameters.getConfigFileName());
+    }
+
+    @Test
+    void parse_WrongParamName_ParameterException() throws Exception {
+
+        File yamlConfigFile = File.createTempFile("config_test_", ".yaml");
+        // 'cf' was changed to wrong parameter 'cp'
+        String [] params = new String[] {"-cp", yamlConfigFile.getAbsolutePath()};
+
+        assertThrows(ParameterException.class, () -> Parameters.parse(params));
+    }
+
+    @Test
+    void parse_FileDoesNotExist_FileNotFoundException() {
+
+        // Config file 'config.yaml' does not exist
+        String [] params = new String[] {"-cf", "config.yaml"};
+
+        assertThrows(FileNotFoundException.class, () -> Parameters.parse(params));
+    }
+}

+ 66 - 0
connector-common/pom.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>connectors</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-common</artifactId>
+    <packaging>jar</packaging>
+    <version>${project.parent.version}</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.9</version>
+        </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>1.24</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-api</artifactId>
+            <version>2.12.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+            <version>2.12.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.everit-org.json-schema</groupId>
+            <artifactId>org.everit.json.schema</artifactId>
+            <version>1.11.1</version>
+        </dependency>
+
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>jitpack.io</id>
+            <url>https://jitpack.io</url>
+        </repository>
+    </repositories>
+
+</project>

+ 49 - 0
connector-common/src/main/java/cz/senslog/connector/config/ConfigurationServiceImpl.java

@@ -0,0 +1,49 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.api.ConfigurationService;
+import cz.senslog.connector.config.model.ConnectorDescriptor;
+import cz.senslog.connector.config.model.DefaultConfig;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The class {@code ConfigurationServiceImpl} represents an implementation of {@link ConfigurationService}.
+ * The class is used to provide a registration for some new connectors and class configurations.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public abstract class ConfigurationServiceImpl implements ConfigurationService {
+
+    private Set<ConnectorDescriptor> connectorDescriptors;
+    private Map<Class<?>, DefaultConfig> configurations;
+
+    ConfigurationServiceImpl() {
+        this.connectorDescriptors = new HashSet<>();
+        this.configurations = new HashMap<>();
+    }
+
+    protected void addConnectorDescriptor(ConnectorDescriptor descriptor) {
+        connectorDescriptors.add(descriptor);
+    }
+
+    protected void addProviderConfiguration(Class<?> aClass, DefaultConfig config) {
+        if (!configurations.containsKey(aClass)) {
+            configurations.put(aClass, config);
+        }
+    }
+
+    @Override
+    public Set<ConnectorDescriptor> getConnectorDescriptors() {
+        return connectorDescriptors;
+    }
+
+    @Override
+    public DefaultConfig getConfigForClass(Class<?> aClass) {
+        return configurations.get(aClass);
+    }
+}

+ 46 - 0
connector-common/src/main/java/cz/senslog/connector/config/DatabaseBuilderImpl.java

@@ -0,0 +1,46 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.api.DatabaseBuilder;
+import cz.senslog.connector.config.api.DatabaseConfigurationService;
+
+/**
+ * The class {@code DatabaseBuilderImpl} represents an implementation of {@link DatabaseBuilder}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public 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;
+
+    @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 DatabaseConfigurationService build() {
+        return new DatabaseConfigurationServiceImpl(connectionUrl, username, password);
+    }
+}

+ 47 - 0
connector-common/src/main/java/cz/senslog/connector/config/DatabaseConfigurationServiceImpl.java

@@ -0,0 +1,47 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.api.DatabaseConfigurationService;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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 extends ConfigurationServiceImpl implements 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;
+
+    /**
+     * Constructor sets all attributes.
+     * @param connectionUrl - connection url.
+     * @param username - username.
+     * @param password - password.
+     */
+    DatabaseConfigurationServiceImpl(String connectionUrl, String username, String password) {
+        this.connectionUrl = connectionUrl;
+        this.username = username;
+        this.password = password;
+    }
+
+    @Override
+    public void connect() {
+        throw logger.throwing(new UnsupportedOperationException(format(
+                "%s#connect() is not implemented.", DatabaseConfigurationServiceImpl.class
+        )));
+    }
+}

+ 42 - 0
connector-common/src/main/java/cz/senslog/connector/config/FileBuilderImpl.java

@@ -0,0 +1,42 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.api.FileBuilder;
+import cz.senslog.connector.config.api.FileConfigurationService;
+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
+ */
+public 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;
+    }
+}

+ 263 - 0
connector-common/src/main/java/cz/senslog/connector/config/FileConfigurationServiceImpl.java

@@ -0,0 +1,263 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.api.FileConfigurationService;
+import cz.senslog.connector.config.model.ConnectorDescriptor;
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.exception.UnsupportedFileException;
+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.*;
+
+/**
+ * The class {@code FileConfigurationServiceImpl} represents an implementation of {@link FileConfigurationService}.
+ *
+ * Configuration file is in YAML format and contains two major groups: 'settings' and 'connectors'.
+ *
+ * <h2>connectors</h2>
+ * Each connector must contain following attributes:
+ *  - name of connector
+ *  - ID of fetch provider (ID is mentioned in 'settings' group)
+ *  - ID of push provider (ID is mentioned in 'settings' group)
+ *  - period in second when the connector will be scheduled
+ *
+ *  Example:
+ *
+ *      connectors:
+ *          - ConnectorName:
+ *              fetcher: "<id>"
+ *              pusher: "<id>"
+ *              period: 60
+ *
+ *
+ * <h2>settings</h2>
+ * Each provider must contain basic attributes:
+ *  - identifier of provider
+ *  - name of provider
+ *  - provider class
+ *
+ *  Other attributes are dynamically loaded when they are needed
+ *  but must keep the key and value syntax.
+ *
+ *  Example:
+ *
+ *      settings:
+ *          - ProviderID:
+ *              name: "<name>"
+ *              provider: "cz.senslog.connector.ClassProvider"
+ *
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class FileConfigurationServiceImpl extends ConfigurationServiceImpl implements FileConfigurationService {
+
+    private static Logger logger = LogManager.getLogger(FileConfigurationServiceImpl.class);
+
+    /** 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;
+    }
+
+    @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");
+        }
+
+        logger.debug("Opening the file '{}'.", fileName);
+        InputStream fileStream = Files.newInputStream(filePath);
+
+        logger.debug("Parsing the yaml file '{}'.", fileName);
+        Map<Object, Object> properties = new Yaml().load(fileStream);
+        logger.debug("The configuration yaml file '{}' was parsed successfully.", fileName);
+
+        logger.debug("Getting 'settings' property from the configuration file.");
+        List settingsList = (List)properties.get("settings");
+
+        logger.debug("Getting 'connectors' property from the configuration file.");
+        List connectorsList = (List) properties.get("connectors");
+
+        logger.debug("Starting to parse all connector descriptors from the config file.");
+        Map<String, Class<?>> settings = settings(settingsList);
+
+        logger.debug("Starting to create all connector connection from the configuration file.");
+        createConnectorDescriptors(connectorsList, settings);
+
+        logger.info("The configuration file '{}' was parsed successfully.", fileName);
+    }
+
+    private Map<String, Class<?>> settings(List settingsList) throws InvalidPropertiesFormatException {
+        Map<String, Class<?>> idDescClass = new HashMap<>();
+
+        logger.debug("Parsing 'settings' from the configuration file.");
+        for (Object settings : settingsList) {
+            if (!(settings instanceof Map)) {
+                throw logger.throwing(new InvalidPropertiesFormatException(
+                        "Property 'settings' is not in the valid format."
+                ));
+            }
+
+            Map settingsMap = (Map) settings;
+            for (Object settingsEntryObject : settingsMap.entrySet()) {
+                if (!(settingsEntryObject instanceof Map.Entry)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values in property 'settings' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map.Entry settingsEntry = (Map.Entry) settingsEntryObject;
+
+                String descriptorId = (String) settingsEntry.getKey();
+
+                logger.debug("Getting descriptor for the settings ID '{}'.", descriptorId);
+                Object settingsValuesObject = settingsEntry.getValue();
+                if (!(settingsValuesObject instanceof Map)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values for the descriptor '"+descriptorId+"' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map settingsValuesMap = (Map) settingsValuesObject;
+
+                logger.debug("Getting property 'provider' from the settings descriptor '{}'.", descriptorId);
+                String providerClassStr = (String) settingsValuesMap.get("provider");
+                if (providerClassStr == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Property 'provider' was not found."
+                    ));
+                }
+                settingsValuesMap.remove("provider");
+
+                Class<?> providerClass;
+                try {
+                    logger.debug("Creating a class from the provider class name {}.", providerClassStr);
+                    providerClass = Class.forName(providerClassStr);
+                } catch (ClassNotFoundException e) {
+                    logger.catching(e);
+                    continue;
+                }
+
+                logger.debug("Creating a new DefaultConfig class for the settings descriptor '{}'.", descriptorId);
+                DefaultConfig defaultConfig = new DefaultConfig(descriptorId, providerClass);
+
+                logger.debug("Starting to set all properties from the settings descriptor '{}'.", descriptorId);
+                for (Object propertyEntryObject : settingsValuesMap.entrySet()) {
+                    if (!(propertyEntryObject instanceof Map.Entry)) {
+                        throw logger.throwing(new InvalidPropertiesFormatException(
+                                        "Property values in the descriptor '"+descriptorId+"' are not accessible as a dictionary."
+                        ));
+                    }
+
+                    Map.Entry propertyEntry = (Map.Entry) propertyEntryObject;
+
+                    logger.trace("Setting property '{}' from the settings descriptor '{}'.", propertyEntry.getKey(), descriptorId);
+                    defaultConfig.setProperty((String) propertyEntry.getKey(), propertyEntry.getValue());
+                }
+
+                logger.debug("Saving the settings descriptor '{}'.", descriptorId);
+                addProviderConfiguration(providerClass, defaultConfig);
+
+                idDescClass.put(descriptorId, providerClass);
+            }
+        }
+        return idDescClass;
+    }
+
+    private void createConnectorDescriptors(List connectorsList, Map<String, Class<?>> settings) throws InvalidPropertiesFormatException {
+
+        logger.debug("Parsing 'connectors' from the configuration file.");
+        for (Object connector : connectorsList) {
+            if (!(connector instanceof Map)) {
+                throw logger.throwing(new InvalidPropertiesFormatException(
+                        "Property 'connectors' is not in the valid format."
+                ));
+            }
+
+            Map connectorMap = (Map) connector;
+
+            for (Object connectorEntryObject : connectorMap.entrySet()) {
+                if (!(connectorEntryObject instanceof Map.Entry)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values in property 'connectors' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map.Entry connectorEntry = (Map.Entry) connectorEntryObject;
+
+                String descriptorId = (String) connectorEntry.getKey();
+
+                logger.debug("Getting descriptor for the connector ID '{}'.", descriptorId);
+                Object connectorValuesObject = connectorEntry.getValue();
+                if (!(connectorValuesObject instanceof Map)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values for the descriptor '"+descriptorId+"' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map connectorValuesMap = (Map) connectorValuesObject;
+
+                logger.debug("Getting the fetch class provider for the connector ID '{}'.", descriptorId);
+                String fetchProviderId = (String) connectorValuesMap.get("fetcher");
+                if (fetchProviderId == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Property 'fetcher' does not exist in connector descriptor '"+descriptorId+"'."
+                    ));
+                }
+
+                Class<?> fetchProviderClass = settings.get(fetchProviderId);
+                if (fetchProviderClass == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Identifier for property 'fetcher' was not found in settings descriptors."
+                    ));
+                }
+
+                logger.debug("Getting the push class provider for the connector ID '{}'.", descriptorId);
+                String pushProviderId = (String) connectorValuesMap.get("pusher");
+                if (pushProviderId == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Property 'pusher' does not exist in connector descriptor '"+descriptorId+"'."
+                    ));
+                }
+
+                Class<?> pushProviderClass = settings.get(pushProviderId);
+                if (pushProviderClass == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Identifier for property 'pusher' was not found in settings descriptors."
+                    ));
+                }
+
+                logger.debug("Getting property 'period' from the connector descriptor '{}'.", descriptorId);
+                Integer period = (Integer) connectorValuesMap.get("period");
+
+                logger.debug("Creating a new ConnectorDescriptor class for the connector descriptor '{}'.", descriptorId);
+                ConnectorDescriptor connDesc = new ConnectorDescriptor(descriptorId, fetchProviderClass, pushProviderClass, period);
+
+                logger.debug("Saving the connector descriptor '{}'.", descriptorId);
+                addConnectorDescriptor(connDesc);
+            }
+        }
+    }
+}

+ 51 - 0
connector-common/src/main/java/cz/senslog/connector/config/api/ConfigurationService.java

@@ -0,0 +1,51 @@
+package cz.senslog.connector.config.api;
+
+import cz.senslog.connector.config.DatabaseBuilderImpl;
+import cz.senslog.connector.config.FileBuilderImpl;
+import cz.senslog.connector.config.model.ConnectorDescriptor;
+import cz.senslog.connector.config.model.DefaultConfig;
+
+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 new FileBuilderImpl();
+    }
+
+    /**
+     * Creates a builder for a configuration from a database.
+     * @return new instance of {@link DatabaseBuilder}.
+     */
+    static DatabaseBuilder newDatabaseBuilder() {
+        return new DatabaseBuilderImpl();
+    }
+
+    /**
+     * @return set of connector descriptors.
+     */
+    Set<ConnectorDescriptor> getConnectorDescriptors();
+
+    /**
+     * Returns a configuration depends on input class.
+     * @param aClass - class for which configuration will be gotten.
+     * @return configuration for an input class.
+     */
+    DefaultConfig getConfigForClass(Class<?> aClass);
+}

+ 40 - 0
connector-common/src/main/java/cz/senslog/connector/config/api/DatabaseBuilder.java

@@ -0,0 +1,40 @@
+package cz.senslog.connector.config.api;
+
+
+/**
+ * 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 {
+
+    /**
+     * 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);
+
+    /**
+     * Creates a new instance with the configuration.
+     * @return new instance of {@link DatabaseConfigurationService}.
+     */
+    DatabaseConfigurationService build();
+}

+ 20 - 0
connector-common/src/main/java/cz/senslog/connector/config/api/DatabaseConfigurationService.java

@@ -0,0 +1,20 @@
+package cz.senslog.connector.config.api;
+
+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;
+}

+ 25 - 0
connector-common/src/main/java/cz/senslog/connector/config/api/FileBuilder.java

@@ -0,0 +1,25 @@
+package cz.senslog.connector.config.api;
+
+/**
+ * The interface {@code FileBuilder} provides a configuration
+ * to load a configuration file.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface FileBuilder {
+
+    /**
+     * 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();
+}

+ 22 - 0
connector-common/src/main/java/cz/senslog/connector/config/api/FileConfigurationService.java

@@ -0,0 +1,22 @@
+package cz.senslog.connector.config.api;
+
+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;
+}

+ 61 - 0
connector-common/src/main/java/cz/senslog/connector/config/model/ConnectorDescriptor.java

@@ -0,0 +1,61 @@
+package cz.senslog.connector.config.model;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code ConnectorDescriptor} represents a configuration class for a connector.
+ * According to this descriptor is created a new connector.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class ConnectorDescriptor {
+
+    /** Name of a connector. */
+    private final String name;
+
+    /** Class of a fetcher. */
+    private final Class<?> fetcher;
+
+    /** class of a pusher. */
+    private final Class<?> pusher;
+
+    /** Period for scheduling. */
+    private final Integer period;
+
+    /**
+     * Constructor sets all attributes.
+     * @param name - name of a connector.
+     * @param fetcher - class of a fetcher.
+     * @param pusher - class of a pusher.
+     * @param period - period for scheduling.
+     */
+    public ConnectorDescriptor(String name, Class<?> fetcher, Class<?> pusher, Integer period) {
+        this.name = name;
+        this.fetcher = fetcher;
+        this.pusher = pusher;
+        this.period = period;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Class<?> getFetcher() {
+        return fetcher;
+    }
+
+    public Class<?> getPusher() {
+        return pusher;
+    }
+
+    public Integer getPeriod() {
+        return period;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 42 - 0
connector-common/src/main/java/cz/senslog/connector/config/model/DefaultConfig.java

@@ -0,0 +1,42 @@
+package cz.senslog.connector.config.model;
+
+/**
+ * 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<?> getProvider() {
+        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();
+    }
+}

+ 52 - 0
connector-common/src/main/java/cz/senslog/connector/config/model/HostConfig.java

@@ -0,0 +1,52 @@
+package cz.senslog.connector.config.model;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code HostConfig} represents a configuration class.
+ * Contains basic information which are needed to create an url.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HostConfig {
+
+    /** Domain of the host. */
+    private final String domain;
+
+    /** Path of the host. */
+    private final String path;
+
+    /**
+     * Constructor sets all attributes.
+     * @param domain - domain of the host.
+     * @param path - path of the host.
+     */
+    public HostConfig(String domain, String path) {
+        this.domain = domain;
+        this.path = path;
+    }
+
+    /**
+     * Constructor sets all attributes from generic property class {@link PropertyConfig}.
+     * @param config - generic configuration.
+     */
+    public HostConfig(PropertyConfig config) {
+        this.domain = config.getStringProperty("domain");
+        this.path = config.getStringProperty("path");
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 135 - 0
connector-common/src/main/java/cz/senslog/connector/config/model/PropertyConfig.java

@@ -0,0 +1,135 @@
+package cz.senslog.connector.config.model;
+
+import cz.senslog.connector.exception.PropertyNotFoundException;
+import cz.senslog.connector.util.ClassUtils;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.lang.String.format;
+import static java.time.ZoneOffset.UTC;
+
+/**
+ * The class {@code PropertyConfig} represents a general configuration class.
+ * Contains map of properties which represents a tree of configuration.
+ * Each node is a {@code PropertyConfig} which contains {@see #id}
+ * and could be generally located. Each leave can be represented
+ * as {@see Integer}, {@see String} or {@see LocalDateTime}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+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 void setProperty(String name, Object value) {
+        properties.put(name, 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))
+        );
+    }
+
+    /**
+     * 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 value = getProperty(name);
+
+        if (value instanceof LocalDateTime) {
+            return (LocalDateTime) value;
+        }
+
+        if (value instanceof Date) {
+            Date date = (Date) value;
+            return date.toInstant().atZone(UTC).toLocalDateTime();
+        }
+
+        throw new ClassCastException(format(
+                "Property '%s' can not be cast to %s", getNewPropertyId(name), LocalDateTime.class)
+        );
+    }
+
+    /**
+     * 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<String, Object> properties = (Map<String, Object>) property;
+            for (Map.Entry<String, Object> propertyEntry : properties.entrySet()) {
+                config.setProperty(propertyEntry.getKey(), propertyEntry.getValue());
+            }
+        }
+
+        return config;
+    }
+
+    private String getNewPropertyId(String name) {
+        return id + PATH_DELIMITER + name;
+    }
+
+    public String getId() {
+        return id;
+    }
+}

+ 8 - 0
connector-common/src/main/java/cz/senslog/connector/exception/PropertyNotFoundException.java

@@ -0,0 +1,8 @@
+package cz.senslog.connector.exception;
+
+public class PropertyNotFoundException extends RuntimeException {
+
+    public PropertyNotFoundException(String message) {
+        super(message);
+    }
+}

+ 8 - 0
connector-common/src/main/java/cz/senslog/connector/exception/SyntaxException.java

@@ -0,0 +1,8 @@
+package cz.senslog.connector.exception;
+
+public class SyntaxException extends RuntimeException {
+
+    public SyntaxException(String message) {
+        super(message);
+    }
+}

+ 10 - 0
connector-common/src/main/java/cz/senslog/connector/exception/UnsupportedFileException.java

@@ -0,0 +1,10 @@
+package cz.senslog.connector.exception;
+
+import java.io.IOException;
+
+public class UnsupportedFileException extends IOException {
+
+    public UnsupportedFileException(String message) {
+        super(message);
+    }
+}

+ 8 - 0
connector-common/src/main/java/cz/senslog/connector/http/ContentType.java

@@ -0,0 +1,8 @@
+package cz.senslog.connector.http;
+
+public class ContentType {
+
+    public static final String APPLICATION_JSON = "application/json; charset=utf-8";
+
+    public static final String TEXT_PLAIN = "text/plain";
+}

+ 182 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpClient.java

@@ -0,0 +1,182 @@
+package cz.senslog.connector.http;
+
+import cz.senslog.connector.util.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpMessage;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cz.senslog.connector.http.HttpCode.*;
+import static org.apache.http.HttpHeaders.*;
+
+/**
+ * The class {@code HttpClient} represents a wrapper for {@link org.apache.http.client.HttpClient}.
+ * Provides functionality of sending GET and POST request. Otherwise is returned response with {@see #BAD_REQUEST}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpClient {
+
+    /** Instance of http client. */
+    private final org.apache.http.client.HttpClient client;
+
+    /**
+     * Factory method to create a new instance of client.
+     * @return new instance of {@code HttpClient}.
+     */
+    public static HttpClient newHttpClient() {
+        return new HttpClient();
+    }
+
+    /**
+     * Private constructors sets http client.
+     */
+    private HttpClient() {
+        this.client = HttpClientBuilder.create().build();
+    }
+
+    /**
+     * Sends http request.
+     * @param request - virtual request.
+     * @return virtual response.
+     */
+    public HttpResponse send(HttpRequest request) {
+        try {
+            switch (request.getMethod()) {
+                case GET:  return sendGet(request);
+                case POST: return sendPost(request);
+                default: return HttpResponse.newBuilder()
+                            .body("Request does not contain method definition.")
+                            .status(METHOD_NOT_ALLOWED).build();
+            }
+        } catch (URISyntaxException e) {
+            return HttpResponse.newBuilder()
+                    .body(e.getMessage()).status(BAD_REQUEST)
+                    .build();
+        } catch (IOException e) {
+            return  HttpResponse.newBuilder()
+                    .body(e.getMessage()).status(SERVER_ERROR)
+                    .build();
+        }
+    }
+
+    /**
+     * Sends GET request.
+     * @param request - virtual request.
+     * @return virtual response of the request.
+     * @throws URISyntaxException throws if host url is not valid.
+     * @throws IOException throws if anything happen during sending.
+     */
+    private HttpResponse sendGet(HttpRequest request) throws IOException, URISyntaxException {
+
+        URI uri = request.getUrl().toURI();
+        HttpGet requestGet = new HttpGet(uri);
+        setBasicHeaders(request, requestGet);
+
+        org.apache.http.HttpResponse responseGet = client.execute(requestGet);
+
+        HttpResponse response = HttpResponse.newBuilder()
+                .status(responseGet.getStatusLine().getStatusCode())
+                .headers(getHeaders(responseGet))
+                .body(getBody(responseGet.getEntity()))
+                .build();
+
+        EntityUtils.consume(responseGet.getEntity());
+
+        return response;
+    }
+
+    /**
+     * Sends POST request.
+     * @param request - virtual request.
+     * @return virtual response of the request.
+     * @throws URISyntaxException throws if host url is not valid.
+     * @throws IOException throws if anything happen during sending.
+     */
+    private HttpResponse sendPost(HttpRequest request) throws URISyntaxException, IOException {
+
+        URI uri = request.getUrl().toURI();
+        HttpPost requestPost = new HttpPost(uri);
+        setBasicHeaders(request, requestPost);
+
+        if (StringUtils.isNotBlank(request.getContentType())) {
+            requestPost.setHeader(CONTENT_TYPE, request.getContentType());
+        }
+
+        requestPost.setEntity(new StringEntity(request.getBody()));
+
+        org.apache.http.HttpResponse responsePost = client.execute(requestPost);
+
+        HttpResponse response = HttpResponse.newBuilder()
+                .headers(getHeaders(requestPost))
+                .status(responsePost.getStatusLine().getStatusCode())
+                .body(getBody(responsePost.getEntity()))
+                .build();
+
+        EntityUtils.consume(responsePost.getEntity());
+
+        return response;
+    }
+
+    /**
+     * Sets basic headers to each request.
+     * @param userRequest - virtual request.
+     * @param httpRequest - real request prepared to send.
+     */
+    private void setBasicHeaders(HttpRequest userRequest, HttpRequestBase httpRequest) {
+
+        httpRequest.setHeader(USER_AGENT, "SenslogConnector/1.0");
+        httpRequest.setHeader(CACHE_CONTROL, "no-cache");
+
+        for (Map.Entry<String, String> headerEntry : userRequest.getHeaders().entrySet()) {
+            httpRequest.setHeader(headerEntry.getKey(), headerEntry.getValue());
+        }
+    }
+
+    /**
+     * Returns map of headers from the response.
+     * @param response - response message.
+     * @return map of headers.
+     */
+    private Map<String, String> getHeaders(HttpMessage response) {
+        Map<String, String> headers = new HashMap<>();
+        for (Header header : response.getAllHeaders()) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    /**
+     * Returns body from the response.
+     * @param entity - response entity.
+     * @return string body of the response.
+     * @throws IOException can not get body from the response.
+     */
+    private String getBody(HttpEntity entity) throws IOException {
+        InputStream contentStream = entity.getContent();
+        InputStreamReader bodyStream = new InputStreamReader(contentStream);
+        BufferedReader rd = new BufferedReader(bodyStream);
+        StringBuilder bodyBuffer = new StringBuilder();
+        String line;
+        while ((line = rd.readLine()) != null) {
+            bodyBuffer.append(line);
+        }
+        return bodyBuffer.toString();
+    }
+}

+ 20 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpCode.java

@@ -0,0 +1,20 @@
+package cz.senslog.connector.http;
+
+public class HttpCode {
+
+    public static final int OK = 200;
+
+    public static final int NO_CONTENT = 204;
+
+    public static final int BAD_REQUEST = 400;
+
+    public static final int UNAUTHORIZED = 401;
+
+    public static final int FORBIDDEN = 403;
+
+    public static final int NOT_FOUND = 404;
+
+    public static final int METHOD_NOT_ALLOWED = 405;
+
+    public static final int SERVER_ERROR = 500;
+}

+ 6 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpHeader.java

@@ -0,0 +1,6 @@
+package cz.senslog.connector.http;
+
+public class HttpHeader {
+
+    public static final String AUTHORIZATION = "Authorization";
+}

+ 5 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpMethod.java

@@ -0,0 +1,5 @@
+package cz.senslog.connector.http;
+
+public enum  HttpMethod {
+    GET, POST
+}

+ 101 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpRequest.java

@@ -0,0 +1,101 @@
+package cz.senslog.connector.http;
+
+import java.net.URL;
+import java.util.Map;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code HttpRequest} represents a wrapper for a http request.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpRequest {
+
+    public interface Builder {
+        Builder header(String name, String value);
+        Builder url(URL url);
+        Builder POST();
+        Builder GET();
+        Builder contentType(String contentType);
+        Builder body(String body);
+        HttpRequest build();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpRequest}.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder() {
+        return new HttpRequestBuilder();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpRequest}.
+     * @param url - host url.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder(URL url) {
+        HttpRequestBuilder builder = new HttpRequestBuilder();
+        builder.url(url);
+        return builder;
+    }
+
+    /** Request url. */
+    private final URL url;
+
+    /** Request headers. */
+    private final Map<String, String> headers;
+
+    /** Request body. */
+    private final String body;
+
+    /** Request method. */
+    private final HttpMethod method;
+
+    /** Request content type. */
+    private final String contentType;
+
+    /**
+     * Constructors sets all attributes.
+     * @param url - url.
+     * @param headers - headers.
+     * @param body - body.
+     * @param method - method.
+     * @param contentType - content type.
+     */
+    HttpRequest(URL url, Map<String, String> headers, String body, HttpMethod method, String contentType) {
+        this.url = url;
+        this.headers = headers;
+        this.body = body;
+        this.method = method;
+        this.contentType = contentType;
+    }
+
+    public URL getUrl() {
+        return url;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public HttpMethod getMethod() {
+        return method;
+    }
+
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 69 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpRequestBuilder.java

@@ -0,0 +1,69 @@
+package cz.senslog.connector.http;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The class {@code HttpRequestBuilder} represents a builder for the {@link HttpRequest}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+final class HttpRequestBuilder implements HttpRequest.Builder {
+
+    private URL url;
+    private Map<String, String> headers;
+    private String body;
+    private HttpMethod method;
+    private String contentType;
+
+    HttpRequestBuilder() {
+        this.headers = new HashMap<>();
+        this.method = HttpMethod.GET;
+        this.body = "";
+    }
+
+
+    @Override
+    public HttpRequest.Builder header(String name, String value) {
+        this.headers.put(name, value);
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder url(URL url) {
+        this.url = url;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder POST() {
+        this.method = HttpMethod.POST;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder GET() {
+        this.method = HttpMethod.GET;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder contentType(String contentType) {
+        this.contentType = contentType;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder body(String body) {
+        this.body = body;
+        return this;
+    }
+
+    @Override
+    public HttpRequest build() {
+        return new HttpRequest(url, headers, body, method, contentType);
+    }
+}

+ 77 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpResponse.java

@@ -0,0 +1,77 @@
+package cz.senslog.connector.http;
+
+import java.util.Map;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code HttpResponse} represents a wrapper for a http response.
+ * Contains basic information like status, headers and body.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpResponse {
+
+    public interface Builder {
+        Builder body(String body);
+        Builder headers(Map<String, String> headers);
+        Builder status(int status);
+        HttpResponse build();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpResponse}.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder() {
+        return new HttpResponseBuilder();
+    }
+
+    /** Response body. */
+    private final String body;
+
+    /** Response headers. */
+    private final Map<String, String> headers;
+
+    /** Response status. */
+    private final int status;
+
+    /**
+     * Constructors sets all attributes.
+     * @param body - body.
+     * @param headers - headers.
+     * @param status - status.
+     */
+    HttpResponse(String body, Map<String, String> headers, int status) {
+        this.body = body;
+        this.headers = headers;
+        this.status = status;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public String getHeader(String value) {
+        return headers.get(value);
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public boolean isOk() {
+        return status == HttpCode.OK;
+    }
+
+    public boolean isError() {
+        return !isOk();
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 42 - 0
connector-common/src/main/java/cz/senslog/connector/http/HttpResponseBuilder.java

@@ -0,0 +1,42 @@
+package cz.senslog.connector.http;
+
+import java.util.Map;
+
+/**
+ * The class {@code HttpResponseBuilder} represents a builder for the {@link HttpResponse}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class HttpResponseBuilder implements HttpResponse.Builder {
+
+    private String body;
+    private Map<String, String> headers;
+    private int status;
+
+    HttpResponseBuilder(){}
+
+    @Override
+    public HttpResponse.Builder body(String body) {
+        this.body = body;
+        return this;
+    }
+
+    @Override
+    public HttpResponse.Builder headers(Map<String, String> headers) {
+        this.headers = headers;
+        return this;
+    }
+
+    @Override
+    public HttpResponse.Builder status(int status) {
+        this.status = status;
+        return this;
+    }
+
+    @Override
+    public HttpResponse build() {
+        return new HttpResponse(body, headers, status);
+    }
+}

+ 113 - 0
connector-common/src/main/java/cz/senslog/connector/http/URLBuilder.java

@@ -0,0 +1,113 @@
+package cz.senslog.connector.http;
+
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import static java.net.URLEncoder.encode;
+
+/**
+ * The class {@code URLBuilder} represents a builder to create a new instance of {@link URL}.
+ * Provides a creating a url from domain and path and adding a parameter.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class URLBuilder {
+
+    /**
+     * Factory method to create a new instance of {@code URLBuilder} from base url.
+     * @param baseURL - host url.
+     * @return new instance of {@code URLBuilder}.
+     */
+    public static URLBuilder newBuilder(String baseURL) {
+        return new URLBuilder(baseURL);
+    }
+
+    /**
+     * Factory method to create a new instance of {@code URLBuilder} from domain and path.
+     * Normalizes domain and path to the form:
+     *
+     * domain: http://domain.com/
+     * path: /host
+     * -> url: http://domain.com/host
+     *
+     * domain: http://domain.com
+     * path: host
+     * -> url: http://domain.com/host
+     *
+     * @param domain - domain of host.
+     * @param path - path of host.
+     * @return new instance of {@code URLBuilder}.
+     */
+    public static URLBuilder newBuilder(String domain, String path) {
+        boolean domainSlash = domain.endsWith("/");
+        boolean pathSlash = path.startsWith("/");
+
+        if ((domainSlash && !pathSlash) || (!domainSlash && pathSlash)) {
+            return new URLBuilder(domain + path);
+        } else if (domainSlash) {
+            return new URLBuilder(domain + path.substring(1));
+        } else {
+            return new URLBuilder(domain + "/" + path);
+        }
+    }
+
+    /** String builder for url. */
+    private StringBuilder urlBuilder;
+
+    /** String builder for parameters. */
+    private StringBuilder paramsBuilder;
+
+    /**
+     * Private constructor initializes builders and normalizes url.
+     * If the url ends with slash '/', it is removed.
+     * @param baseURL - host url.
+     */
+    private URLBuilder(String baseURL) {
+        String url = baseURL.endsWith("/") ? baseURL.substring(0, baseURL.length() - 1) : baseURL;
+        this.urlBuilder = new StringBuilder(url);
+        this.paramsBuilder = new StringBuilder();
+    }
+
+    /**
+     * Adds a new parameter to the url.
+     * @param name - name of parameter.
+     * @param value - value of parameter.
+     * @return instance of {@code URLBuilder}.
+     */
+    public URLBuilder addParam(String name, String value) {
+        try {
+            paramsBuilder.append("&").append(name).append("=").append(encode(value, "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e.getMessage());
+        }
+        return this;
+    }
+
+    /**
+     * Adds a new parameter to the url.
+     * @param name - name of parameter.
+     * @param value - value of parameter.
+     * @return instance of {@code URLBuilder}.
+     */
+    public URLBuilder addParam(String name, Object value) {
+        if (value == null) return this;
+        return addParam(name, value.toString());
+    }
+
+    /**
+     * Creates a new instance of {@link URL}.
+     * @return new instance of {@link URL}.
+     */
+    public URL build() {
+        try {
+            String params = paramsBuilder.replace(0, 1, "").toString();
+            return new URL(urlBuilder.append(params.isEmpty() ? "" : ("?" + params)).toString());
+        } catch (MalformedURLException e) {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
+}
+

+ 185 - 0
connector-common/src/main/java/cz/senslog/connector/json/BasicJson.java

@@ -0,0 +1,185 @@
+package cz.senslog.connector.json;
+
+import com.google.gson.*;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import cz.senslog.connector.exception.SyntaxException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+
+import static com.google.gson.stream.JsonToken.END_DOCUMENT;
+import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
+
+/**
+ * The class {@code BasicJson} represents a basic wrapper for {@link Gson} library.
+ * Provides basic converter from object to string and string to object.
+ *
+ * Configuration contains basic formatters for {@see LocalDateTime}, {@see ZonedDateTime} and {@see Class}.
+ *
+ *
+ * Both time classes are formatter to ISO format e.q. '2011-12-03T10:15:30',
+ * '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30+01:00[Europe/Paris]'.
+ *
+ * Class is formatted as the full name of the class.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class BasicJson {
+
+    /** Instance of json converter. */
+    private static Gson gson = new GsonBuilder()
+            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+            .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter())
+            .registerTypeAdapter(Class.class, new ClassAdapter())
+            .create();
+
+    /** Formatter for {@see LocalDateTime}. */
+    private static class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
+
+        @Override
+        public JsonElement serialize(LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(localDateTime.format(ISO_DATE_TIME));
+        }
+
+        @Override
+        public LocalDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return LocalDateTime.parse(jsonElement.getAsString(), ISO_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see ZonedDateTime}. */
+    private static class ZonedDateTimeAdapter implements JsonSerializer<ZonedDateTime>, JsonDeserializer<ZonedDateTime> {
+
+        @Override
+        public JsonElement serialize(ZonedDateTime zonedDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(zonedDateTime.format(ISO_DATE_TIME));
+        }
+
+        @Override
+        public ZonedDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return ZonedDateTime.parse(jsonElement.getAsString(), ISO_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see Class}. */
+    private static class ClassAdapter implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {
+
+        @Override
+        public JsonElement serialize(Class<?> aClass, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(aClass.getName());
+        }
+
+        @Override
+        public Class<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            try {
+                return Class.forName(jsonElement.getAsString());
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Deserialize json to a typed object according to class.
+     * @param jsonString - json string.
+     * @param aClass - class of the object.
+     * @param <T> - generic type object.
+     * @return new instance of the input class.
+     */
+    public static <T> T jsonToObject(String jsonString, Class<T> aClass) {
+        try {
+            return gson.fromJson(jsonString, aClass);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Deserialize json to a typed object according to type.
+     * @param jsonString - json string.
+     * @param type - type of the object.
+     * @param <T> - generic type object.
+     * @return new instance of the input type.
+     */
+    public static <T> T jsonToObject(String jsonString, Type type) {
+        try {
+            return gson.fromJson(jsonString, type);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Serialize object to string json.
+     * @param object - input object.
+     * @param <T> - generic type of object.
+     * @return string json.
+     */
+    public static <T> String objectToJson(T object) {
+        try {
+            return gson.toJson(object);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Checks if input string is in json format.
+     * @param json - input json.
+     * @return true - valid, false - invalid.
+     */
+    public static boolean isValid(String json) {
+        return isValid(new JsonReader(new StringReader(json)));
+    }
+
+    /**
+     * Validates input json reader.
+     * @param jsonReader - input json reader.
+     * @return true - valid, false - invalid.
+     */
+    private static boolean isValid(JsonReader jsonReader) {
+        try {
+            JsonToken token;
+            loop:
+            while ( (token = jsonReader.peek()) != END_DOCUMENT && token != null ) {
+                switch ( token ) {
+                    case BEGIN_ARRAY:
+                        jsonReader.beginArray();
+                        break;
+                    case END_ARRAY:
+                        jsonReader.endArray();
+                        break;
+                    case BEGIN_OBJECT:
+                        jsonReader.beginObject();
+                        break;
+                    case END_OBJECT:
+                        jsonReader.endObject();
+                        break;
+                    case NAME:
+                        jsonReader.nextName();
+                        break;
+                    case STRING:
+                    case NUMBER:
+                    case BOOLEAN:
+                    case NULL:
+                        jsonReader.skipValue();
+                        break;
+                    case END_DOCUMENT:
+                        break loop;
+                    default:
+                        throw new AssertionError(token);
+                }
+            }
+            return true;
+        } catch (IOException ignored ) {
+            return false;
+        }
+    }
+
+}

+ 122 - 0
connector-common/src/main/java/cz/senslog/connector/json/JsonSchema.java

@@ -0,0 +1,122 @@
+package cz.senslog.connector.json;
+
+import org.everit.json.schema.Schema;
+import org.everit.json.schema.ValidationException;
+import org.everit.json.schema.loader.SchemaLoader;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static java.lang.String.format;
+import static java.nio.file.Files.readAllBytes;
+
+/**
+ * The class {@code JsonSchema} represents a basic wrapper for {@link Schema}.
+ * Provides functionality of creating a new schema and validation an input json according the schema.
+ * Input json can start as a list or object.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class JsonSchema {
+
+    /** Instance of loaded schema. */
+    private final Schema schema;
+
+    /**
+     * Loads the schema from resources.
+     * @param schemaName - name of the schema.
+     * @return new instance of {@code JsonSchema}.
+     * @throws IOException throws if the schema does not exist or can not be loaded.
+     */
+    public static JsonSchema loadAsResource(String schemaName) throws IOException {
+        InputStream inputStream = ClassLoader.getSystemResourceAsStream(schemaName);
+
+        if (inputStream == null) {
+            throw new IOException(format("Resource file %s was not found.", schemaName));
+        }
+
+        BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputStream));
+        String schema = streamReader.lines().collect(Collectors.joining());
+
+        return create(schema);
+    }
+
+    /**
+     * Loads the schema from file system.
+     * @param schemaPath - path of the schema.
+     * @return new instance of {@code JsonSchema}.
+     * @throws IOException throws if the schema does not exist or can not be loaded.
+     */
+    public static JsonSchema load(Path schemaPath) throws IOException {
+        return create(new String(readAllBytes(schemaPath)));
+    }
+
+    /**
+     *  Creates and build a new schema.
+     * @param jsonSchema - string json schema.
+     * @return new instance of {@code JsonSchema}.
+     */
+    public static JsonSchema create(String jsonSchema) {
+        Schema schema = SchemaLoader.builder()
+                .schemaJson(new JSONObject(jsonSchema)).build()
+                .load().build();
+        return new JsonSchema(schema);
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static method {@link JsonSchema#create(String)}.
+     * @param schema - build schema.
+     */
+    private JsonSchema(Schema schema) {
+        this.schema = schema;
+    }
+
+    /**
+     * Validates input json which starts as an object.
+     * @param json - input object json.
+     * @param errors - list of errors if json is not valid.
+     * @return true - valid, false - invalid.
+     */
+    public boolean validateJsonObject(String json, List<String> errors) {
+        return validate(new JSONObject(json), errors);
+    }
+
+    /**
+     * Validates input json which starts as an array.
+     * @param json - input array json.
+     * @param errors - list of errors if json is not valid.
+     * @return true - valid, false - invalid.
+     */
+    public boolean validateJsonArray(String json, List<String> errors) {
+        return validate(new JSONArray(json), errors);
+    }
+
+    /**
+     * Validates input json according to build schema. If input json is not valid,
+     * is thrown an exception with messages why json is not valid.
+     * @param json - input json.
+     * @param errors - list of errors if json is not valid.
+     * @return true - valid, false - invalid.
+     */
+    private boolean validate(Object json, List<String> errors) {
+        try {
+            schema.validate(json);
+            return true;
+        } catch (ValidationException e) {
+            if (errors != null) {
+                errors.clear();
+                errors.addAll(e.getAllMessages());
+            }
+            return false;
+        }
+    }
+}

+ 30 - 0
connector-common/src/main/java/cz/senslog/connector/util/ClassUtils.java

@@ -0,0 +1,30 @@
+package cz.senslog.connector.util;
+
+import static java.lang.String.format;
+
+/**
+ * The class {@code ClassUtils} represents set of tools for classes.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class ClassUtils {
+
+    /**
+     * Provides type safe functionality of casting. Can be used only in case,
+     * if is casted a value which is direct type of the class (inheritance is not supported).
+     *
+     * @param value - value to be casted.
+     * @param castClass - class for casting.
+     * @param <T> - generic type of casting.
+     * @return casted value.
+     */
+    public static <T> T cast(Object value, Class<T> castClass) {
+        if (value.getClass().equals(castClass) ) {
+            return (T) value;
+        } else {
+            throw new ClassCastException(format("Value '%s' can not be cast to %s", value, castClass));
+        }
+    }
+}

+ 31 - 0
connector-common/src/main/java/cz/senslog/connector/util/Next.java

@@ -0,0 +1,31 @@
+package cz.senslog.connector.util;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * The interface {@code Next} provides right side of pipeline.
+ * Pipeline can be ended by calling {@code Next#end} or can be chained by calling {@code Next#next}.
+ *
+ * @param <T> - generic type of consuming data
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Next<T> {
+
+    /**
+     * Ends pipeline and consumes received data.
+     * @param end - consumer of received data.
+     */
+    void end(Consumer<T> end);
+
+    /**
+     * Provides chain for the pipeline. Parameter is function received data and provides new data.
+     * @param next - function consumes data and provides new data.
+     * @param <R> - generic type of new data.
+     * @return - new instance of {@link Pipe}.
+     */
+    <R> Pipe<R> next(Function<? super T, ? extends R> next);
+}

+ 33 - 0
connector-common/src/main/java/cz/senslog/connector/util/NextImpl.java

@@ -0,0 +1,33 @@
+package cz.senslog.connector.util;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * The class {@code NextImpl} represents implementation of {@link Next}.
+ *
+ * @param <R> - generic type of data.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class NextImpl<R> implements Next<R> {
+
+    /** Received data from pipeline */
+    private final R data;
+
+    NextImpl(R data) {
+        this.data = data;
+    }
+
+    @Override
+    public void end(Consumer<R> end) {
+        end.accept(data);
+    }
+
+    @Override
+    public <T> Pipe<T> next(Function<? super R, ? extends T> next) {
+        return new PipeImpl<>(next.apply(data));
+    }
+}

+ 25 - 0
connector-common/src/main/java/cz/senslog/connector/util/NumberUtils.java

@@ -0,0 +1,25 @@
+package cz.senslog.connector.util;
+
+/**
+ * The class {@code NumberUtils} represents set of tools for numbers.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class NumberUtils {
+
+    /**
+     * Converts Integer value to Float value. Uses functionality of {@see Float#valueOf}
+     * and is extended of null value checker.
+     * Example:
+     *          null    -> null
+     *          10      -> 10.0
+     *
+     * @param value - integer value.
+     * @return float value.
+     */
+    public static Float valueOf(Integer value) {
+        return value == null ? null : Float.valueOf(value);
+    }
+}

+ 20 - 0
connector-common/src/main/java/cz/senslog/connector/util/Pipe.java

@@ -0,0 +1,20 @@
+package cz.senslog.connector.util;
+
+import java.util.function.Function;
+
+/**
+ * The interface {@code Pipe} provides functionality of pipeline {@see Pipeline}.
+ * The method {@code Pipe#pipe} represents character '|'.
+ *
+ * @param <T> - generic type of input data.
+ */
+public interface Pipe<T> {
+
+    /**
+     * Converter represents '|' of idea of pipeline.
+     * @param mapper - converter for pipeline's flow.
+     * @param <R> - generic type of output data.
+     * @return new instance of {@link Next}.
+     */
+    <R> Next<R> pipe(Function<? super T, ? extends R> mapper);
+}

+ 27 - 0
connector-common/src/main/java/cz/senslog/connector/util/PipeImpl.java

@@ -0,0 +1,27 @@
+package cz.senslog.connector.util;
+
+import java.util.function.Function;
+
+/**
+ * The class {@code PipeImpl} represents implementation of {@link Pipe}.
+ *
+ * @param <T> - generic type of data.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class PipeImpl<T> implements Pipe<T> {
+
+    /** Received data from pipeline */
+    private final T data;
+
+    PipeImpl(T data) {
+        this.data = data;
+    }
+
+    @Override
+    public <R> Next<R> pipe(Function<? super T, ? extends R> mapper) {
+        return new NextImpl<>(mapper.apply(data));
+    }
+}

+ 38 - 0
connector-common/src/main/java/cz/senslog/connector/util/Pipeline.java

@@ -0,0 +1,38 @@
+package cz.senslog.connector.util;
+
+import java.util.function.Supplier;
+
+/**
+ * The interface {@code Pipeline} provides generic functionality of pipeline.
+ * This interfaces creates start of a pipeline and provides {@link Pipe}.
+ *
+ * Idea of pipeline:
+ *
+ *  of | end
+ *  of | next | end
+ *  of | next | next | end
+ *
+ * Basic example:
+ *
+ *  Pipeline.of(<provide_data>).pipe(<convert_data>).end(<consume_data>);
+ *
+ *  Advanced example:
+ *
+ *  Pipeline.of(<provide_data>).pipe(<convert_data>).next(<consume_and_provide_new_data>).pipe(<convert_data>).end(<consume_data>);
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Pipeline {
+
+    /**
+     * Start method of pipeline.
+     * @param start - supplier provides data.
+     * @param <T> - generic type of data.
+     * @return new instance of {@link Pipe}.
+     */
+    static <T> Pipe<T> of(Supplier<T> start) {
+        return new PipeImpl<>(start.get());
+    }
+}

+ 60 - 0
connector-common/src/main/java/cz/senslog/connector/util/StringUtils.java

@@ -0,0 +1,60 @@
+package cz.senslog.connector.util;
+
+/**
+ * The class {@code StringUtils} represents set of tools for strings.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class StringUtils {
+
+    /**
+     * Test if input string is null or empty.
+     * Examples:
+     *          null -> true
+     *          ""   -> true
+     *          " "  -> false
+     *          "a"  -> false
+     *
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isEmpty(String string) {
+        return string == null || string.isEmpty();
+    }
+
+    /**
+     * Tests if input string is null, empty or contains white characters.
+     * Examples:
+     *          null   -> true
+     *          ""     -> true
+     *          " "    -> true
+     *          "    " -> true
+     *          "a"    -> false
+     *
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isBlank(String string) {
+        return string == null || string.replaceAll("\\s+","").isEmpty();
+    }
+
+    /**
+     *  Tests negative functionality of {@see StringUtils#isEmpty}.
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isNotEmpty(String string) {
+        return !isEmpty(string);
+    }
+
+    /**
+     *  Tests negative functionality of {@see StringUtils#isBlank}.
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isNotBlank(String string) {
+        return !isBlank(string);
+    }
+}

+ 63 - 0
connector-common/src/main/java/cz/senslog/connector/util/Triple.java

@@ -0,0 +1,63 @@
+package cz.senslog.connector.util;
+
+/**
+ * The class {@code Triple} represents an accumulator for three values.
+ * Each value can be in different type.
+ *
+ * @param <A> type of the first value.
+ * @param <B> type of the second value.
+ * @param <C> type of the third value.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class Triple<A, B, C> {
+
+    /** First value. */
+    private final A item1;
+
+    /** Second value. */
+    private final B item2;
+
+    /** Third value. */
+    private final C item3;
+
+    /**
+     * Factory method to create a new instance.
+     * @param item1 first value.
+     * @param item2 second value.
+     * @param item3 third value.
+     * @param <A> type of the first value.
+     * @param <B> type of the second value.
+     * @param <C> type of the third value.
+     * @return new instance of {@code Triple}.
+     */
+    public static <A, B, C> Triple of(A item1, B item2, C item3) {
+        return new Triple<A, B, C>(item1, item2, item3);
+    }
+
+    /**
+     * Constructor of the class sets all attributes.
+     * @param item1 first value.
+     * @param item2 second value.
+     * @param item3 third value.
+     */
+    private Triple(A item1, B item2, C item3) {
+        this.item1 = item1;
+        this.item2 = item2;
+        this.item3 = item3;
+    }
+
+    public A getItem1() {
+        return item1;
+    }
+
+    public B getItem2() {
+        return item2;
+    }
+
+    public C getItem3() {
+        return item3;
+    }
+}

+ 51 - 0
connector-common/src/main/java/cz/senslog/connector/util/Tuple.java

@@ -0,0 +1,51 @@
+package cz.senslog.connector.util;
+
+/**
+ * The class {@code Tuple} represents an accumulator for two values.
+ * Each value can be in different type.
+ *
+ * @param <A> type of the first value.
+ * @param <B> type of the second value.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class Tuple<A, B> {
+
+    /** First value. */
+    private final A item1;
+
+    /** Second value */
+    private final B item2;
+
+    /**
+     * Factory method to create a new instance.
+     * @param item1 first value.
+     * @param item2 second value.
+     * @param <A> type of the first value.
+     * @param <B> type of the second value.
+     * @return new instance of {@code Tuple}.
+     */
+    public static <A, B> Tuple of(A item1, B item2) {
+        return new Tuple<>(item1, item2);
+    }
+
+    /**
+     * Constructor of the class sets all attributes.
+     * @param item1 first value.
+     * @param item2 second value.
+     */
+    private Tuple(A item1, B item2) {
+        this.item1 = item1;
+        this.item2 = item2;
+    }
+
+    public A getItem1() {
+        return item1;
+    }
+
+    public B getItem2() {
+        return item2;
+    }
+}

+ 20 - 0
connector-common/src/main/resources/log4j2.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="INFO">
+    <Appenders>
+        <!-- Console Appender -->
+        <Console name="STDOUT" target="SYSTEM_OUT">
+            <PatternLayout pattern="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n" />
+        </Console>
+
+        <File name="FileLog" fileName="logs/all.log" immediateFlush="false" append="false">
+            <PatternLayout pattern="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n" />
+        </File>
+    </Appenders>
+    <Loggers>
+        <Logger name="cz.senslog" level="info" />
+        <Root level="info">
+            <AppenderRef ref="STDOUT" />
+            <AppenderRef ref="FileLog" />
+        </Root>
+    </Loggers>
+</Configuration>

+ 18 - 0
connector-common/src/test/java/cz/senslog/connector/config/FileBuilderImplTest.java

@@ -0,0 +1,18 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.api.ConfigurationService;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class FileBuilderImplTest {
+
+    @Test
+    void build() {
+
+        ConfigurationService service = ConfigurationService.newFileBuilder()
+                .fileName("test.yaml").build();
+
+        assertEquals(FileConfigurationServiceImpl.class, service.getClass());
+    }
+}

+ 67 - 0
connector-common/src/test/java/cz/senslog/connector/config/FileConfigurationServiceImplTest.java

@@ -0,0 +1,67 @@
+package cz.senslog.connector.config;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.config.api.FileConfigurationService;
+import cz.senslog.connector.config.model.ConnectorDescriptor;
+import cz.senslog.connector.exception.UnsupportedFileException;
+import org.junit.jupiter.api.Test;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class FileConfigurationServiceImplTest {
+
+
+    @Test
+    void testParsingConfigFile() throws IOException, ClassNotFoundException, URISyntaxException {
+
+        URI uri = ClassLoader.getSystemResource("test_valid_config.yaml").toURI();
+        String configFileName = Paths.get(uri).toString();
+
+        FileConfigurationService configService = new FileConfigurationServiceImpl(configFileName);
+        configService.load();
+
+        Set<ConnectorDescriptor> descriptors = configService.getConnectorDescriptors();
+
+        assertEquals(1, descriptors.size());
+
+        ConnectorDescriptor descriptor = descriptors.iterator().next();
+        assertEquals("ConnectorName", descriptor.getName());
+        assertEquals(TestFetchProviderClass.class, descriptor.getFetcher());
+        assertEquals(TestPushProviderClass.class, descriptor.getPusher());
+        assertEquals(100, descriptor.getPeriod());
+
+        DefaultConfig fetchConfig = configService.getConfigForClass(TestFetchProviderClass.class);
+        assertEquals("FetchProviderId", fetchConfig.getId());
+        assertEquals(TestFetchProviderClass.class, fetchConfig.getProvider());
+        assertEquals("<name>", fetchConfig.getStringProperty("name"));
+
+        DefaultConfig pushConfig = configService.getConfigForClass(TestPushProviderClass.class);
+        assertEquals("PushProviderId", pushConfig.getId());
+        assertEquals(TestPushProviderClass.class, pushConfig.getProvider());
+        assertEquals("<name>", pushConfig.getStringProperty("name"));
+    }
+
+    @Test
+    void load_WrongFileNameExtension_UnsupportedFileException() {
+
+        FileConfigurationService service = new FileConfigurationServiceImpl("test.txt");
+
+        assertThrows(UnsupportedFileException.class, service::load);
+    }
+
+    @Test
+    void load_FileDoesNotExist_FileNotFoundException() {
+
+        FileConfigurationService service = new FileConfigurationServiceImpl("test.yaml");
+
+        assertThrows(FileNotFoundException.class, service::load);
+    }
+}

+ 4 - 0
connector-common/src/test/java/cz/senslog/connector/config/TestFetchProviderClass.java

@@ -0,0 +1,4 @@
+package cz.senslog.connector.config;
+
+public class TestFetchProviderClass {
+}

+ 4 - 0
connector-common/src/test/java/cz/senslog/connector/config/TestPushProviderClass.java

@@ -0,0 +1,4 @@
+package cz.senslog.connector.config;
+
+public class TestPushProviderClass {
+}

+ 18 - 0
connector-common/src/test/java/cz/senslog/connector/config/api/DefaultConfigTest.java

@@ -0,0 +1,18 @@
+package cz.senslog.connector.config.api;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DefaultConfigTest {
+
+    @Test
+    void hashCode_ProviderEqual_True() {
+
+        DefaultConfig config1 = new DefaultConfig("1", DefaultConfig.class);
+        DefaultConfig config2 = new DefaultConfig("2", DefaultConfig.class);
+
+        assertEquals(config1, config2);
+    }
+}

+ 21 - 0
connector-common/src/test/java/cz/senslog/connector/config/model/ConnectorDescriptorTest.java

@@ -0,0 +1,21 @@
+package cz.senslog.connector.config.model;
+
+import org.junit.jupiter.api.Test;
+
+import static cz.senslog.connector.json.BasicJson.jsonToObject;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ConnectorDescriptorTest {
+
+    @Test
+    void toString_ConvertJson_True() {
+
+        String jsonDescriptor = new ConnectorDescriptor("test", ConnectorDescriptorTest.class, ConnectorDescriptorTest.class, 42).toString();
+        ConnectorDescriptor descriptor = jsonToObject(jsonDescriptor, ConnectorDescriptor.class);
+
+        assertEquals("test", descriptor.getName());
+        assertEquals(ConnectorDescriptorTest.class, descriptor.getFetcher());
+        assertEquals(ConnectorDescriptorTest.class, descriptor.getPusher());
+        assertEquals(42, descriptor.getPeriod());
+    }
+}

+ 19 - 0
connector-common/src/test/java/cz/senslog/connector/config/model/HostConfigTest.java

@@ -0,0 +1,19 @@
+package cz.senslog.connector.config.model;
+
+import org.junit.jupiter.api.Test;
+
+import static cz.senslog.connector.json.BasicJson.jsonToObject;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class HostConfigTest {
+
+    @Test
+    void toString_ConvertJson_True() {
+
+        String jsonConfig = new HostConfig("http://test.com", "test").toString();
+        HostConfig config = jsonToObject(jsonConfig, HostConfig.class);
+
+        assertEquals("http://test.com", config.getDomain());
+        assertEquals("test", config.getPath());
+    }
+}

+ 66 - 0
connector-common/src/test/java/cz/senslog/connector/config/model/PropertyConfigTest.java

@@ -0,0 +1,66 @@
+package cz.senslog.connector.config.model;
+
+import cz.senslog.connector.exception.PropertyNotFoundException;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.util.Date;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class PropertyConfigTest {
+
+    @Test
+    void property_basicDataTypes_True() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("string", "testString");
+        config.setProperty("integer", 42);
+
+        LocalDateTime localDateTime = LocalDateTime.of(1970, Month.JANUARY, 1, 0,0, 0);
+        config.setProperty("localDateTime", localDateTime);
+
+        Date date = Date.from(LocalDate.of( 1970 , 1 , 1).atStartOfDay(ZoneOffset.UTC).toInstant());
+        config.setProperty("date", date);
+
+        assertEquals("test", config.getId());
+        assertEquals("testString", config.getStringProperty("string"));
+        assertEquals(42, config.getIntegerProperty("integer"));
+        assertEquals(localDateTime, config.getLocalDateTimeProperty("localDateTime"));
+        assertEquals(localDateTime, config.getLocalDateTimeProperty("date"));
+    }
+
+    @Test
+    void property_configProperty_True() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("values", new HashMap<String, Integer>(){{put("integer", 42);}});
+
+        PropertyConfig valuesConfig = config.getPropertyConfig("values");
+
+        assertEquals("test/values", valuesConfig.getId());
+        assertEquals(42, valuesConfig.getIntegerProperty("integer"));
+    }
+
+    @Test
+    void property_NotFound_PropertyNotFoundException() {
+
+        PropertyConfig config = new PropertyConfig("test");
+
+        assertThrows(PropertyNotFoundException.class, () -> config.getProperty("unknown"));
+    }
+
+    @Test
+    void property_InvalidDataType_ClassCastException() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("date", 42);
+
+        assertThrows(ClassCastException.class, () -> config.getLocalDateTimeProperty("date"));
+    }
+}

+ 111 - 0
connector-common/src/test/java/cz/senslog/connector/http/URLBuilderTest.java

@@ -0,0 +1,111 @@
+package cz.senslog.connector.http;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URL;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class URLBuilderTest {
+
+
+    @Test
+    void build_BasicUrl_True() {
+        URL url = URLBuilder.newBuilder("http://test.com").build();
+        assertEquals("http://test.com", url.toString());
+    }
+
+    @Test
+    void build_OneParam_True() {
+
+        URL url = URLBuilder.newBuilder("http://test.com")
+                .addParam("name", "value")
+                .build();
+
+        assertEquals("test.com", url.getHost());
+        assertEquals("http://test.com?name=value", url.toString());
+    }
+
+    @Test
+    void build_TwoParams_True() {
+
+        URL url = URLBuilder.newBuilder("http://test.com")
+                .addParam("first", "value")
+                .addParam("second", "value")
+                .build();
+
+        assertEquals("test.com", url.getHost());
+        assertEquals("http://test.com?first=value&second=value", url.toString());
+    }
+
+    @Test
+    void build_EndUrlSlash_True() {
+
+        URL url = URLBuilder.newBuilder("http://test.com/").build();
+        assertEquals("http://test.com", url.toString());
+    }
+
+    @Test
+    void build_OneParamAndUrlEndSlash_True() {
+
+        URL url = URLBuilder.newBuilder("http://test.com/")
+                .addParam("name", "value")
+                .build();
+
+        assertEquals("test.com", url.getHost());
+        assertEquals("http://test.com?name=value", url.toString());
+    }
+
+    @Test
+    void build_UrlEndSlashAndDomainBeginSlash_True() {
+
+        URL url = URLBuilder.newBuilder("http://test.com/", "/test").build();
+        assertEquals("http://test.com/test", url.toString());
+    }
+
+    @Test
+    void build_UrlEndSlash_True() {
+        URL url = URLBuilder.newBuilder("http://test.com/", "test").build();
+        assertEquals("http://test.com/test", url.toString());
+    }
+
+    @Test
+    void build_DomainBeginSlash_True() {
+        URL url = URLBuilder.newBuilder("http://test.com", "/test").build();
+        assertEquals("http://test.com/test", url.toString());
+    }
+
+    @Test
+    void build_WithoutSlash_True() {
+        URL url = URLBuilder.newBuilder("http://test.com", "test").build();
+        assertEquals("http://test.com/test", url.toString());
+    }
+
+    @Test
+    void build_OneParamAndUrlEndSlashAndDomainBeginSlash_True() {
+        URL url = URLBuilder.newBuilder("http://test.com/", "/test/")
+                .addParam("name", "value")
+                .build();
+        assertEquals("http://test.com/test?name=value", url.toString());
+    }
+
+    @Test
+    void build_OneObjectParam_True() {
+
+        URL url = URLBuilder.newBuilder("http://test.com")
+                .addParam("number", 42)
+                .build();
+
+        assertEquals("test.com", url.getHost());
+        assertEquals("http://test.com?number=42", url.toString());
+    }
+
+    @Test
+    void build_UnsupportedUrl_IllegalArgumentException() {
+
+        URLBuilder builder = URLBuilder.newBuilder("unsupported_url");
+
+        assertThrows(IllegalArgumentException.class, builder::build);
+    }
+}

+ 26 - 0
connector-common/src/test/java/cz/senslog/connector/util/ClassUtilsTest.java

@@ -0,0 +1,26 @@
+package cz.senslog.connector.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ClassUtilsTest {
+
+    @Test
+    void cast_SupportedCast_True() {
+
+        Object stringObj = "test";
+
+        String string = ClassUtils.cast(stringObj, String.class);
+
+        assertEquals("test", string);
+    }
+
+    @Test
+    void cast_UnsupportedCast_Exception() {
+
+        Object numberObj = "1";
+
+        assertThrows(ClassCastException.class, () -> ClassUtils.cast(numberObj, Integer.class));
+    }
+}

+ 28 - 0
connector-common/src/test/java/cz/senslog/connector/util/NumberUtilsTest.java

@@ -0,0 +1,28 @@
+package cz.senslog.connector.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class NumberUtilsTest {
+
+    @Test
+    void valueOf_IntegerToFloat_Float() {
+
+        Integer input = 10;
+
+        Float result = NumberUtils.valueOf(input);
+
+        assertEquals(10, result);
+    }
+
+    @Test
+    void valueOf_IntegerToNull_Null() {
+
+        Integer input = null;
+
+        Float result = NumberUtils.valueOf(input);
+
+        assertNull(result);
+    }
+}

+ 29 - 0
connector-common/src/test/java/cz/senslog/connector/util/PipelineTest.java

@@ -0,0 +1,29 @@
+package cz.senslog.connector.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class PipelineTest {
+
+
+    @Test
+    void testPipe() {
+
+        Supplier<Integer> producer = () -> 42;
+        Function<Integer, Integer> converter = num -> num * 2;
+        Function<Integer, Integer> stepConsumer = num -> num + 16;
+        Consumer<Integer> endConsumer = num -> {throw new NumberFormatException(num.toString());};
+
+        // ((42 * 2) + 16 ) * 2 = 200
+        NumberFormatException exception = assertThrows(NumberFormatException.class,
+                () -> Pipeline.of(producer).pipe(converter).next(stepConsumer).pipe(converter).end(endConsumer));
+
+        assertEquals("200", exception.getMessage());
+    }
+}

+ 36 - 0
connector-common/src/test/java/cz/senslog/connector/util/StringUtilsTest.java

@@ -0,0 +1,36 @@
+package cz.senslog.connector.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class StringUtilsTest {
+
+    @Test
+    void isEmpty() {
+        assertTrue(StringUtils.isEmpty(""));
+        assertTrue(StringUtils.isEmpty(null));
+        assertFalse(StringUtils.isEmpty(" "));
+        assertFalse(StringUtils.isEmpty("a"));
+    }
+
+    @Test
+    void isBlank() {
+        assertTrue(StringUtils.isBlank(""));
+        assertTrue(StringUtils.isBlank(null));
+        assertTrue(StringUtils.isBlank(" "));
+        assertTrue(StringUtils.isBlank("   "));
+        assertTrue(StringUtils.isBlank("        "));
+        assertFalse(StringUtils.isBlank("a"));
+    }
+
+    @Test
+    void isNotEmpty() {
+        assertTrue(StringUtils.isNotEmpty("a"));
+    }
+
+    @Test
+    void isNotBlank() {
+        assertTrue(StringUtils.isNotBlank("a"));
+    }
+}

+ 18 - 0
connector-common/src/test/java/cz/senslog/connector/util/TripleTest.java

@@ -0,0 +1,18 @@
+package cz.senslog.connector.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TripleTest {
+
+    @Test
+    void of_ValidParams_True() {
+
+        Triple input = Triple.of("test4", 42, "test2");
+
+        assertEquals("test4", input.getItem1());
+        assertEquals(42, input.getItem2());
+        assertEquals("test2", input.getItem3());
+    }
+}

+ 17 - 0
connector-common/src/test/java/cz/senslog/connector/util/TupleTest.java

@@ -0,0 +1,17 @@
+package cz.senslog.connector.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TupleTest {
+
+    @Test
+    void of_ValidParams_True() {
+
+        Tuple input = Tuple.of("test", 42);
+
+        assertEquals("test", input.getItem1());
+        assertEquals(42, input.getItem2());
+    }
+}

+ 14 - 0
connector-common/src/test/resources/test_valid_config.yaml

@@ -0,0 +1,14 @@
+settings:
+  - FetchProviderId:
+      name: "<name>"
+      provider: "cz.senslog.connector.config.TestFetchProviderClass"
+
+  - PushProviderId:
+      name: "<name>"
+      provider: "cz.senslog.connector.config.TestPushProviderClass"
+
+connectors:
+  - ConnectorName:
+      fetcher: "FetchProviderId"
+      pusher: "PushProviderId"
+      period: 100

+ 38 - 0
connector-fetch-api/pom.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>connectors</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-fetch-api</artifactId>
+    <version>${project.parent.version}</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-common</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-model</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 61 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/ConnectorFetch.java

@@ -0,0 +1,61 @@
+package cz.senslog.connector.fetch;
+
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+
+/**
+ * The class {@code ConnectorFetch} represents a loader for classes implement {@link ConnectorFetchProvider}.
+ * For this is used technology Java Service Provider Interface (SPI).
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ConnectorFetch {
+
+    private static Logger logger = LogManager.getLogger(ConnectorFetch.class);
+
+    /** Map of implementations. */
+    private static Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> services;
+
+    static {
+        services = loadAll();
+    }
+
+    /**
+     * Loads and saves all available implementations of {@link ConnectorFetchProvider}.
+     * @return Map of all implementations.
+     */
+    private static Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> loadAll() {
+        logger.debug("Getting all implementation of the class {}.", ConnectorFetchProvider.class);
+        Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> services = new HashMap<>();
+        ServiceLoader<ConnectorFetchProvider> loader = ServiceLoader.load(ConnectorFetchProvider.class);
+        for (ConnectorFetchProvider connectorProvider : loader) {
+            logger.debug("Loaded the class {}.", connectorProvider.getClass());
+            services.put(connectorProvider.getClass(), connectorProvider);
+        }
+        logger.info("Successfully loaded {} class of the {}.",  services.size(), ConnectorFetchProvider.class);
+        return services;
+    }
+
+    /**
+     * Returns all implementations of {@link ConnectorFetchProvider} where
+     * the key is the class and the value is concrete implementation.
+     * @return Map of all implementations.
+     */
+    public static Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> provideAll() {
+        return services;
+    }
+
+    /**
+     * Returns an implementation of provider according to input class.
+     * @param providerClass - class which is find an implementation.
+     * @return implementation type to {@link ConnectorFetchProvider}.
+     */
+    public static ConnectorFetchProvider getProvider(Class<?> providerClass) {
+        return services.get(providerClass);
+    }
+}

+ 21 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetchProvider.java

@@ -0,0 +1,21 @@
+package cz.senslog.connector.fetch.api;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+
+/**
+ * The interface {@code ConnectorFetchProvider} provides a generic communication interface to create a new fetcher.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConnectorFetchProvider {
+
+    /**
+     * Creates a new instance of {@link ConnectorFetcher}. This method receive default
+     * configuration {@link DefaultConfig} which is used to configure the new instance of fetcher.
+     * @param config - default configuration.
+     * @return new instance of {@link ConnectorFetcher}.
+     */
+    ConnectorFetcher createFetcher(DefaultConfig config);
+}

+ 27 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetcher.java

@@ -0,0 +1,27 @@
+package cz.senslog.connector.fetch.api;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+/**
+ * The interface {@code ConnectorFetcher} provides a generic communication interface for fetchers.
+ *
+ * @param <T> generic parameter of model which the method 'fetch' sens as an output.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConnectorFetcher<T extends AbstractModel> {
+
+    /**
+     * Initialization of fetcher. Method is called only once when is created a new connector.
+     * @throws Exception throws when initialization is not successful.
+     */
+    void init() throws Exception;
+
+    /**
+     * Method is periodically scheduled and contains logic of fetcher.
+     * @return model of data which was fetched.
+     */
+    T fetch();
+}

+ 37 - 0
connector-fetch-azure/pom.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>connectors</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-fetch-azure</artifactId>
+    <version>${project.parent.version}</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-fetch-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-common</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 63 - 0
connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/AzureConfig.java

@@ -0,0 +1,63 @@
+package cz.senslog.connector.fetch.azure;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.config.model.HostConfig;
+import cz.senslog.connector.fetch.azure.auth.AzureAuthConfig;
+
+import java.time.LocalDateTime;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code AzureConfig} represents a configuration class for the {@link AzureFetcher}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AzureConfig {
+
+    private final LocalDateTime startDate;
+    private final Integer limitPerSensor;
+    private final HostConfig sensorInfoHost;
+    private final HostConfig sensorDataHost;
+    private final AzureAuthConfig authentication;
+
+    /**
+     * Constructor sets class attributes from default input configuration class {@link DefaultConfig}.
+     * From the default configuration are dynamically get properties for attributes.
+     * @param config - default configuration
+     */
+    public AzureConfig(DefaultConfig config) {
+        this.startDate = config.getLocalDateTimeProperty("startDate");
+        this.limitPerSensor = config.getIntegerProperty("limitPerSensor");
+        this.sensorInfoHost = new HostConfig(config.getPropertyConfig("sensorInfoHost"));
+        this.sensorDataHost = new HostConfig(config.getPropertyConfig("sensorDataHost"));
+        this.authentication = new AzureAuthConfig(config.getPropertyConfig("authentication"));
+    }
+
+    public LocalDateTime getStartDate() {
+        return startDate;
+    }
+
+    public Integer getLimitPerSensor() {
+        return limitPerSensor;
+    }
+
+    public HostConfig getSensorInfoHost() {
+        return sensorInfoHost;
+    }
+
+    public HostConfig getSensorDataHost() {
+        return sensorDataHost;
+    }
+
+    public AzureAuthConfig getAuthentication() {
+        return authentication;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 248 - 0
connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/AzureFetcher.java

@@ -0,0 +1,248 @@
+package cz.senslog.connector.fetch.azure;
+
+import com.google.gson.reflect.TypeToken;
+import cz.senslog.connector.config.model.HostConfig;
+import cz.senslog.connector.exception.SyntaxException;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.fetch.azure.auth.AzureAuthenticationService;
+import cz.senslog.connector.http.HttpClient;
+import cz.senslog.connector.http.HttpRequest;
+import cz.senslog.connector.http.HttpResponse;
+import cz.senslog.connector.http.URLBuilder;
+import cz.senslog.connector.json.JsonSchema;
+import cz.senslog.connector.model.azure.AzureModel;
+import cz.senslog.connector.model.azure.SensorData;
+import cz.senslog.connector.model.azure.SensorInfo;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static cz.senslog.connector.http.HttpHeader.AUTHORIZATION;
+import static cz.senslog.connector.json.BasicJson.jsonToObject;
+import static cz.senslog.connector.util.StringUtils.isBlank;
+import static java.lang.String.format;
+import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
+import static java.util.Collections.emptyList;
+
+/**
+ * The class {@code AzureFetcher} represents an implementation of {@link ConnectorFetcher}.
+ * The class contains {@link AzureModel} which contains fetched data.
+ *
+ * <h2>Initialization</h2>
+ * At first are loaded both schemas for validation received JSONs.
+ * When schemas were loaded successfully, is triggered the authentication process to get access token.
+ * The token is used to get information of sensors. If loading fails or are not get any sensors,
+ * will be throw an exception. Otherwise the sensors are saved.
+ *
+ * <h2>Fetch</h2>
+ * If list of sensors is empty, is returned empty model.
+ * For each sensor are get data which is valid by schema and parsed from json format to object.
+ * If the process is successful, data is saved to the sensor and is get the time of last update.
+ * All data of sensors is send in model and the time of last fetch is changed according to time of the last update.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AzureFetcher implements ConnectorFetcher<AzureModel> {
+
+    private static Logger logger = LogManager.getLogger(AzureFetcher.class);
+
+
+    /** Name of the sensor info JSON schema. */
+    private static final String SENSOR_INFO_JSON_SCHEMA_NAME = "schema/sensorInfoSchema.json";
+
+    /** Name of the sensor data JSON schema. */
+    private static final String SENSOR_DATA_JSON_SCHEMA_NAME = "schema/sensorDataSchema.json";
+
+
+    /** Configuration. */
+    private final AzureConfig config;
+
+    /** Service for authentication. */
+    private final AzureAuthenticationService authService;
+
+    /** Http client. */
+    private final HttpClient httpClient;
+
+
+    /** Time when new data will be fetched. */
+    private LocalDateTime lastFetch;
+
+    /** List of sensor information. */
+    private List<SensorInfo> sensorInfos;
+
+    /** Schema for sensor data. */
+    private JsonSchema sensorDataSchema;
+
+    /**
+     * Constructor of the class sets all attributes.
+     * @param config - configuration for fetcher.
+     * @param authService - authentication service.
+     * @param httpClient - http client.
+     */
+    AzureFetcher(AzureConfig config, AzureAuthenticationService authService, HttpClient httpClient) {
+        this.config = config;
+        this.authService = authService;
+        this.httpClient = httpClient;
+        this.lastFetch = config.getStartDate();
+        this.sensorInfos = new ArrayList<>();
+    }
+
+    @Override
+    public void init() throws Exception {
+
+        logger.debug("Creating a new schema {}.", SENSOR_DATA_JSON_SCHEMA_NAME);
+        sensorDataSchema = JsonSchema.loadAsResource(SENSOR_DATA_JSON_SCHEMA_NAME);
+        logger.info("Schema {} was created successfully.", SENSOR_DATA_JSON_SCHEMA_NAME);
+
+        logger.debug("Creating a new schema {}.", SENSOR_INFO_JSON_SCHEMA_NAME);
+        JsonSchema sensorInfoSchema = JsonSchema.loadAsResource(SENSOR_INFO_JSON_SCHEMA_NAME);
+        logger.info("Schema {} was created successfully.", SENSOR_INFO_JSON_SCHEMA_NAME);
+
+        String accessToken = authService.getAccessToken();
+
+        HostConfig sensorInfoHost = config.getSensorInfoHost();
+        logger.info("Creating a http request to {}.", sensorInfoHost);
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(sensorInfoHost.getDomain(), sensorInfoHost.getPath()).build())
+                .header(AUTHORIZATION, accessToken)
+                .build();
+
+        logger.info("Sending the http request to get information about sensors.");
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {}.", response.getStatus());
+
+        if (response.isError()) {
+            throw logger.throwing(new Exception(format(
+                    "Can not get information about the sensors. %s", response.getBody()
+            )));
+        }
+
+        String bodyJson = response.getBody();
+        List<String> errors = new ArrayList<>();
+        if (!sensorInfoSchema.validateJsonArray(bodyJson, errors)) {
+            logger.error("{} received sensor info json is not valid according to the schema.", bodyJson);
+            logger.error(Arrays.toString(errors.toArray()));
+            throw logger.throwing(new Exception(format(
+                    "Received json data is not valid according to the schema. %s", bodyJson
+            )));
+        }
+
+        logger.debug("Parsing body of the response from JSON format.");
+        Type sensorInfoListType = new TypeToken<Collection<SensorInfo>>() {}.getType();
+        List<SensorInfo> sensors = jsonToObject(response.getBody(), sensorInfoListType);
+
+        if (sensors.isEmpty()) {
+            throw logger.throwing(new Exception("Received empty list of sensors."));
+        }
+
+        logger.debug("Creating an iterator over all received sensors.");
+        for (SensorInfo sensor : sensors) {
+            logger.debug("Loading a sensor {}.", sensor.getEui());
+            if (sensor.isActive()) {
+                logger.info("Saving an active sensor {}.", sensor.getEui());
+                sensorInfos.add(sensor);
+            } else {
+                logger.info("Sensor {} is not activated.", sensor.getEui());
+            }
+        }
+
+        logger.info("{} sensors were loaded.", sensorInfos.size());
+        logger.info(sensorInfos.toString());
+    }
+
+    @Override
+    public AzureModel fetch() {
+
+        if (sensorInfos.isEmpty()) {
+            logger.error("Sensors information were not loaded. Can not get detailed information.");
+            return new AzureModel(emptyList(), lastFetch, lastFetch);
+        }
+
+        String accessToken = authService.getAccessToken();
+
+        LocalDateTime tempLastDate = lastFetch;
+        int totalFetched = 0;
+
+        for (SensorInfo sensorInfo : sensorInfos) {
+            logger.info("Fetching data for the sensor {}.", sensorInfo.getEui());
+
+            if (isBlank(sensorInfo.getEui())) continue;
+
+            HostConfig sensorDataHost = config.getSensorDataHost();
+            logger.info("Creating a http request to {}.", sensorDataHost);
+            HttpRequest request = HttpRequest.newBuilder()
+                    .url(URLBuilder.newBuilder(sensorDataHost.getDomain(), sensorDataHost.getPath())
+                            .addParam("eui", sensorInfo.getEui())
+                            .addParam("from", lastFetch.format(ISO_DATE_TIME))
+                            .addParam("limit", config.getLimitPerSensor())
+                            .build())
+                    .header(AUTHORIZATION, accessToken)
+                    .GET().build();
+
+            logger.info("Sending the http request.");
+            HttpResponse response = httpClient.send(request);
+            logger.info("Received a response with a status: {}.", response.getStatus());
+
+            if (response.isError()) {
+                logger.error("Can not get data of the sensor {}. Error {} {}",
+                        sensorInfo.getEui(), response.getStatus(), response.getBody());
+                continue;
+            }
+
+            String bodyJson = response.getBody();
+            List<String> errors = new ArrayList<>();
+            if (!sensorDataSchema.validateJsonObject(bodyJson, errors)) {
+                logger.error("{} received sensor data json is not valid according to the schema.", bodyJson);
+                logger.error(Arrays.toString(errors.toArray()));
+                continue;
+            }
+
+            SensorInfo sensorInfoData;
+            try {
+                logger.debug("Parsing body of the response.");
+                sensorInfoData = jsonToObject(bodyJson, SensorInfo.class);
+                logger.info("Received {} records for the sensor {}.", sensorInfoData.getData().size(), sensorInfo.getEui());
+            } catch (SyntaxException e) {
+                logger.catching(e);
+                continue;
+            }
+
+            if (sensorInfoData.getEui().equals(sensorInfo.getEui())) {
+                logger.debug("Setting sensor data to the sensor {}.", sensorInfo.getEui());
+                sensorInfo.setData(sensorInfoData.getData());
+                totalFetched += sensorInfoData.getData().size();
+
+                logger.debug("Getting last record of the sensor data.");
+                SensorData lastRecord = sensorInfoData.getData().get(sensorInfoData.getData().size()-1);
+                LocalDateTime lastRecordTime = lastRecord.getTime().toLocalDateTime();
+                if (lastRecordTime.isAfter(tempLastDate)) {
+                    tempLastDate = lastRecordTime;
+                    logger.info("Time of the last fetched data was changed from {} to {}.",
+                            tempLastDate.format(ISO_DATE_TIME), lastRecordTime.format(ISO_DATE_TIME));
+                }
+            }
+        }
+
+        LocalDateTime from = lastFetch;
+        LocalDateTime to = tempLastDate;
+        logger.info("Fetched data from {} to {}.", from.format(ISO_DATE_TIME), to.format(ISO_DATE_TIME));
+        logger.info("Total fetched {} records.", totalFetched);
+
+        logger.debug("Set new time of fetch from {} to {}.", lastFetch.format(ISO_DATE_TIME), tempLastDate.format(ISO_DATE_TIME));
+        lastFetch = tempLastDate;
+
+        logger.debug("Creating a new instance of Azure model.");
+        AzureModel model = new AzureModel(sensorInfos, from, to);
+        logger.debug("Model was created successfully and sending it forward.");
+
+        return model;
+    }
+}

+ 46 - 0
connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/ConnectorFetchAzureProvider.java

@@ -0,0 +1,46 @@
+package cz.senslog.connector.fetch.azure;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.fetch.azure.auth.AzureAuthConfig;
+import cz.senslog.connector.fetch.azure.auth.AzureAuthenticationService;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import static cz.senslog.connector.fetch.azure.auth.AzureAuthenticationService.newAuthService;
+import static cz.senslog.connector.http.HttpClient.newHttpClient;
+
+/**
+ * The class {@code ConnectorFetchAzureProvider} represents a concrete implementation of {@link ConnectorFetchProvider}.
+ * Contains basic functionality to configure implementation of {@link ConnectorFetcher} from the default configuration.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ConnectorFetchAzureProvider implements ConnectorFetchProvider {
+
+    private static Logger logger = LogManager.getLogger(ConnectorFetchAzureProvider.class);
+
+    @Override
+    public ConnectorFetcher createFetcher(DefaultConfig config) {
+        logger.info("Initialization a new fetch provider {}.", ConnectorFetchAzureProvider.class);
+
+        logger.debug("Creating a new configuration.");
+        AzureConfig azureConfig = new AzureConfig(config);
+        logger.info("Configuration for {} was created successfully.", ConnectorFetcher.class);
+
+        logger.debug("Getting a configuration for authentication.");
+        AzureAuthConfig authConfig = azureConfig.getAuthentication();
+
+        logger.info("Initialization a new Azure authentication service.");
+        AzureAuthenticationService authService = newAuthService(authConfig, newHttpClient());
+
+        logger.debug("Creating a new instance of {}.", AzureFetcher.class);
+        AzureFetcher fetcher = new AzureFetcher(azureConfig, authService, newHttpClient());
+        logger.info("Fetcher for {} was created successfully.", AzureFetcher.class);
+
+        return fetcher;
+    }
+}

+ 50 - 0
connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/auth/AzureAuthConfig.java

@@ -0,0 +1,50 @@
+package cz.senslog.connector.fetch.azure.auth;
+
+import cz.senslog.connector.config.model.HostConfig;
+import cz.senslog.connector.config.model.PropertyConfig;
+
+import java.util.Optional;
+
+import static java.util.Optional.ofNullable;
+
+/**
+ * The class {@code AzureAuthConfig} represents a configuration class for the {@link AzureAuthenticationService}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AzureAuthConfig {
+
+    private final HostConfig host;
+    private final String username;
+    private final String password;
+    private final Optional<Integer> refreshPeriodIfFail;
+
+    /**
+     * Constructor sets class attributes from input configuration class {@link PropertyConfig}.
+     * @param config - configuration
+     */
+    public AzureAuthConfig(PropertyConfig config) {
+        this.host = new HostConfig(config.getPropertyConfig("host"));
+        this.username = config.getStringProperty("username");
+        this.password = config.getStringProperty("password");
+        this.refreshPeriodIfFail = ofNullable(config.getIntegerProperty("refreshPeriodIfFail"));
+    }
+
+    public HostConfig getHost() {
+        return host;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public Optional<Integer> getRefreshPeriodIfFail() {
+        return refreshPeriodIfFail;
+    }
+}

+ 167 - 0
connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/auth/AzureAuthenticationService.java

@@ -0,0 +1,167 @@
+package cz.senslog.connector.fetch.azure.auth;
+
+import com.google.gson.reflect.TypeToken;
+import cz.senslog.connector.config.model.HostConfig;
+import cz.senslog.connector.exception.SyntaxException;
+import cz.senslog.connector.http.HttpClient;
+import cz.senslog.connector.http.HttpRequest;
+import cz.senslog.connector.http.HttpResponse;
+import cz.senslog.connector.http.URLBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.Type;
+import java.time.format.DateTimeParseException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cz.senslog.connector.http.HttpCode.UNAUTHORIZED;
+import static cz.senslog.connector.json.BasicJson.jsonToObject;
+import static cz.senslog.connector.util.StringUtils.isNotBlank;
+import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
+
+/**
+ * The class {@code AzureAuthenticationService} represents a service which wraps
+ * all functionality around authentication. The main method is {@see AzureAuthenticationService#getAccessToken}
+ * and provides a valid access token.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AzureAuthenticationService {
+
+    private static Logger logger = LogManager.getLogger(AzureAuthenticationService.class);
+
+
+    /** Default period value if token can not be refreshed  */
+    private static final Integer DEFAULT_REFRESH_PERIOD = 5000;
+
+
+    /** Configuration */
+    private final AzureAuthConfig authConfig;
+
+    /** Http client */
+    private final HttpClient httpClient;
+
+
+    /** Received authorization information */
+    private AzureAuthorizationInfo authorizationInfo;
+
+    /**
+     * Static factory method to create a new instance of service.
+     * @param authConfig - authentication configuration.
+     * @param httpClient - http client.
+     * @return new instance of {@code AzureAuthenticationService}.
+     */
+    public static AzureAuthenticationService newAuthService(AzureAuthConfig authConfig, HttpClient httpClient) {
+        return new AzureAuthenticationService(authConfig, httpClient);
+    }
+
+    /**
+     * Constructor of the class sets all attributes.
+     * @param authConfig - authentication configuration.
+     * @param httpClient - http client.
+     */
+    private AzureAuthenticationService(AzureAuthConfig authConfig, HttpClient httpClient) {
+        this.authConfig = authConfig;
+        this.httpClient = httpClient;
+    }
+
+    /**
+     * Creates a http request to get actual authorization information.
+     * Request contains credentials (email and password) in header
+     * and are gotten list of access information in json format.
+     * Each record contains token and expire time. The first
+     * non expired token si chosen to use.
+     * @return true or false if token is get successfully and valid.
+     */
+    private boolean refreshToken() {
+
+        HostConfig host = authConfig.getHost();
+        logger.info("Creating a http request to the Azure service at {}.", host);
+        HttpRequest request = HttpRequest.newBuilder()
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath()).build())
+                .header("Email", authConfig.getUsername())
+                .header("Password", authConfig.getPassword())
+                .POST().build();
+
+        logger.info("Sending the http request.");
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {}.", response.getStatus());
+
+        if (response.isError()) {
+            if (response.getStatus() == UNAUTHORIZED) {
+                logger.debug("Parsing response body to a Map.");
+                Map jsonMap = jsonToObject(response.getBody(), Map.class);
+                String message = (String) jsonMap.get("Message");
+                logger.error("Unauthorized {}: {}.", response.getStatus(), message);
+            } else {
+                logger.error("Error with the code {}: {}.", response.getStatus(), response.getBody());
+            }
+            return false;
+        }
+
+        try {
+            logger.debug("Parsing the response body.");
+            Type azureInfoListType = new TypeToken<Collection<AzureAuthorizationInfo>>(){}.getType();
+            List<AzureAuthorizationInfo> azureInfos = jsonToObject(response.getBody(), azureInfoListType);
+
+            if (azureInfos.isEmpty()) {
+                logger.error("Response does not contain authorization information.");
+                return false;
+            }
+
+            logger.info("Response body was parsed successfully and getting authorization information.");
+            for (AzureAuthorizationInfo azureInfo : azureInfos) {
+                if (azureInfo.isAuthorized()) {
+                    authorizationInfo = azureInfo;
+                    return true;
+                }
+            }
+
+            logger.warn("Received authorization information contains only expired tokens.");
+            return false;
+        } catch (SyntaxException | DateTimeParseException e) {
+            logger.catching(e);
+            return false;
+        }
+    }
+
+    /**
+     * Main public method of this service which provides a valid access token.
+     * Content of the method contains a loop where periodically is get access token.
+     * If token is expired or does not exist, it is refreshed. If refresh process fails,
+     * the service goes to sleep and tries it later.
+     * @return string valid access token.
+     */
+    public String getAccessToken() {
+        logger.info("Getting the actual authorization token to Azure.");
+
+        while (true) {
+            if (authorizationInfo != null && authorizationInfo.isAuthorized()) {
+
+                String token = authorizationInfo.getAccessToken();
+
+                if (isNotBlank(token)) {
+                    logger.debug("The access token is valid and will expired at {}.",
+                            authorizationInfo.getExpires().format(ISO_DATE_TIME));
+                    return token;
+                } else {
+                    logger.error("The token '{}' can not be used as an access token.", token);
+                }
+            }
+
+            if (!refreshToken()) {
+                try {
+                    int sleepPeriod = authConfig.getRefreshPeriodIfFail().orElse(DEFAULT_REFRESH_PERIOD);
+                    logger.warn("Can not get valid access token at this moment. Thread is going to sleep for {} ms.", sleepPeriod);
+                    Thread.sleep(sleepPeriod);
+                } catch (InterruptedException e) {
+                    logger.catching(e);
+                }
+            }
+        }
+    }
+}

+ 59 - 0
connector-fetch-azure/src/main/java/cz/senslog/connector/fetch/azure/auth/AzureAuthorizationInfo.java

@@ -0,0 +1,59 @@
+package cz.senslog.connector.fetch.azure.auth;
+
+import java.time.LocalDateTime;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code AzureAuthorizationInfo} represents a transfer object of authorization information.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class AzureAuthorizationInfo {
+
+    /** String hash token. */
+    private String accessToken;
+
+    /** Time when the token expires. */
+    private LocalDateTime expires;
+
+    /**
+     * Check if the token is valid and can be used for authorization.
+     * @return true/false if token expires or not.
+     */
+    public boolean isAuthorized() {
+
+        if (accessToken == null || expires == null) {
+            return false;
+        }
+
+        if (expires.isBefore(LocalDateTime.now())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public LocalDateTime getExpires() {
+       return expires;
+    }
+
+    public void setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+    }
+
+    public void setExpires(LocalDateTime expires) {
+        this.expires = expires;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 1 - 0
connector-fetch-azure/src/main/resources/META-INF/services/cz.senslog.connector.fetch.api.ConnectorFetchProvider

@@ -0,0 +1 @@
+cz.senslog.connector.fetch.azure.ConnectorFetchAzureProvider

+ 76 - 0
connector-fetch-azure/src/main/resources/schema/sensorDataSchema.json

@@ -0,0 +1,76 @@
+{
+  "definitions": {},
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://iotlorawan.azurewebsites.net/sensorInfo.json",
+  "type": "object",
+  "title": "Detailed sensor data",
+  "required": [
+    "eui",
+    "name",
+    "data"
+  ],
+  "properties": {
+    "eui": {
+      "$id": "#/properties/eui",
+      "type": "string",
+      "title": "Eui",
+      "pattern": "^[0-9a-fA-F]+"
+    },
+    "name": {
+      "$id": "#/properties/name",
+      "type": "string",
+      "title": "Name",
+      "pattern": "^(.*)$"
+    },
+    "data": {
+      "$id": "#/properties/data",
+      "type": "array",
+      "title": "The Data Schema",
+      "items": {
+        "$id": "#/properties/data/items",
+        "type": "object",
+        "title": "The Items Schema",
+        "properties": {
+          "time": {
+            "$id": "#/properties/data/items/properties/time",
+            "type": "string",
+            "title": "Time",
+            "description": "Date time when data was measured.",
+            "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})[+](?<zone>[0-9]{2}[:][0-9]{2})"
+          },
+          "temperature": {
+            "$id": "#/properties/data/items/properties/temperature",
+            "type": ["number", "null"],
+            "title": "Temperature"
+          },
+          "humidity": {
+            "$id": "#/properties/data/items/properties/humidity",
+            "type": ["number", "null"],
+            "title": "Humidity"
+          },
+          "co2": {
+            "$id": "#/properties/data/items/properties/co2",
+            "type": ["integer", "null"],
+            "title": "Co2"
+          },
+          "rssi": {
+            "$id": "#/properties/data/items/properties/rssi",
+            "type": ["integer", "null"],
+            "title": "Rssi"
+          },
+          "snr": {
+            "$id": "#/properties/data/items/properties/snr",
+            "type": ["number", "null"],
+            "title": "Snr"
+          },
+          "batteryLevel": {
+            "$id": "#/properties/data/items/properties/batteryLevel",
+            "type": ["number", "null"],
+            "title": "Battery level",
+            "minimum": 0
+          }
+        }
+      }
+    }
+  }
+}

+ 106 - 0
connector-fetch-azure/src/main/resources/schema/sensorInfoSchema.json

@@ -0,0 +1,106 @@
+{
+  "definitions": {},
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://iotlorawan.azurewebsites.net/sensorInfo.json",
+  "type": "array",
+  "title": "List of available sensors",
+  "default": null,
+  "items": {
+    "$id": "#/items",
+    "type": "object",
+    "title": "Sensor info",
+    "description": "Full description of each sensor",
+    "required": [
+      "eui",
+      "acronym",
+      "name",
+      "period",
+      "tolerance",
+      "location",
+      "description",
+      "sensorDataId",
+      "sensorDataTime",
+      "created",
+      "status"
+    ],
+    "properties": {
+      "eui": {
+        "$id": "#/items/properties/eui",
+        "type": "string",
+        "title": "Eui",
+        "pattern": "^[0-9a-fA-F]+"
+      },
+      "acronym": {
+        "$id": "#/items/properties/acronym",
+        "type": "string",
+        "title": "Acronym",
+        "pattern": "^[A-Z]+-[0-9]+"
+      },
+      "name": {
+        "$id": "#/items/properties/name",
+        "type": "string",
+        "title": "Name",
+        "pattern": "^(.*)$"
+      },
+      "period": {
+        "$id": "#/items/properties/period",
+        "type": "integer",
+        "title": "Period",
+        "minimum": 0.0
+      },
+      "tolerance": {
+        "$id": "#/items/properties/tolerance",
+        "type": "integer",
+        "title": "Tolerance"
+      },
+      "location": {
+        "$id": "#/items/properties/location",
+        "type": "string",
+        "title": "Location",
+        "description": "String location of the sensor.",
+        "default": "unknown",
+        "pattern": "^(.*)$"
+      },
+      "description": {
+        "$id": "#/items/properties/description",
+        "type": "string",
+        "title": "Sensor description",
+        "default": "",
+        "pattern": "^(.*)$"
+      },
+      "sensorDataId": {
+        "$id": "#/items/properties/sensorDataId",
+        "type": "integer",
+        "title": "Identifier of the sensor"
+      },
+      "sensorDataTime": {
+        "$id": "#/items/properties/sensorDataTime",
+        "type": "string",
+        "title": "Sensor Data time",
+        "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(.(?<millisecond>[0-9]+))?"
+      },
+      "created": {
+        "$id": "#/items/properties/created",
+        "type": ["string","null"],
+        "title": "Created date time",
+        "description": "Date when the sensor was connected.",
+        "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(.(?<millisecond>[0-9]+))?"
+      },
+      "deleted": {
+        "$id": "#/items/properties/deleted",
+        "type": ["string","null"],
+        "title": "Deleted date time",
+        "description": "Date when the sensor was disconnected.",
+        "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(.(?<millisecond>[0-9]+))?"
+      },
+      "status": {
+        "$id": "#/items/properties/status",
+        "type": "integer",
+        "title": "Status of the sensor",
+        "description": "Signalizes state of the sensor: 1 - active, 0 - inactive.",
+        "minimum": 0.0,
+        "maximum": 1.0
+      }
+    }
+  }
+}

+ 101 - 0
connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/AzureConfigTest.java

@@ -0,0 +1,101 @@
+package cz.senslog.connector.fetch.azure;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+
+import static cz.senslog.connector.json.BasicJson.jsonToObject;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class AzureConfigTest {
+
+    @Test
+    void newConfig_Full_True() {
+
+        DefaultConfig defaultConfig = new DefaultConfig("azure", null);
+        defaultConfig.setProperty("startDate", LocalDateTime.MIN);
+        defaultConfig.setProperty("limitPerSensor", 42);
+        defaultConfig.setProperty("sensorInfoHost", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        defaultConfig.setProperty("sensorDataHost", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        defaultConfig.setProperty("authentication", new HashMap<String, Object>(){{
+            put("username", "username");
+            put("password", "password");
+            put("refreshPeriodIfFail", 100);
+            put("host", new HashMap<String, Object>(){{
+                put("domain", "http://test.com");
+                put("path", "test");
+            }});
+        }});
+
+        AzureConfig config = new AzureConfig(defaultConfig);
+
+        assertEquals(42, config.getLimitPerSensor());
+        assertEquals(LocalDateTime.MIN, config.getStartDate());
+        assertNotNull(config.getSensorInfoHost());
+        assertEquals("http://test.com", config.getSensorInfoHost().getDomain());
+        assertEquals("test", config.getSensorInfoHost().getPath());
+        assertNotNull(config.getSensorDataHost());
+        assertEquals("http://test.com", config.getSensorDataHost().getDomain());
+        assertEquals("test", config.getSensorDataHost().getPath());
+        assertNotNull(config.getAuthentication());
+        assertEquals("username", config.getAuthentication().getUsername());
+        assertEquals("password", config.getAuthentication().getPassword());
+        assertEquals(100, config.getAuthentication().getRefreshPeriodIfFail().orElse(0));
+        assertNotNull(config.getAuthentication().getHost());
+        assertEquals("http://test.com", config.getAuthentication().getHost().getDomain());
+        assertEquals("test", config.getAuthentication().getHost().getPath());
+    }
+
+    @Test
+    void toString_ConvertJson_True() {
+
+        DefaultConfig defaultConfig = new DefaultConfig("azure",null);
+        defaultConfig.setProperty("startDate", LocalDateTime.MIN);
+        defaultConfig.setProperty("limitPerSensor", 42);
+        defaultConfig.setProperty("sensorInfoHost", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        defaultConfig.setProperty("sensorDataHost", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        defaultConfig.setProperty("authentication", new HashMap<String, Object>(){{
+            put("username", "username");
+            put("password", "password");
+            put("refreshPeriodIfFail", 100);
+            put("host", new HashMap<String, Object>(){{
+                put("domain", "http://test.com");
+                put("path", "test");
+            }});
+        }});
+
+        String jsonConfig = new AzureConfig(defaultConfig).toString();
+        AzureConfig config = jsonToObject(jsonConfig, AzureConfig.class);
+
+        assertEquals(42, config.getLimitPerSensor());
+        assertEquals(LocalDateTime.MIN, config.getStartDate());
+        assertNotNull(config.getSensorInfoHost());
+        assertEquals("http://test.com", config.getSensorInfoHost().getDomain());
+        assertEquals("test", config.getSensorInfoHost().getPath());
+        assertNotNull(config.getSensorDataHost());
+        assertEquals("http://test.com", config.getSensorDataHost().getDomain());
+        assertEquals("test", config.getSensorDataHost().getPath());
+        assertNotNull(config.getAuthentication());
+        assertEquals("username", config.getAuthentication().getUsername());
+        assertEquals("password", config.getAuthentication().getPassword());
+        assertEquals(100, config.getAuthentication().getRefreshPeriodIfFail().orElse(0));
+        assertNotNull(config.getAuthentication().getHost());
+        assertEquals("http://test.com", config.getAuthentication().getHost().getDomain());
+        assertEquals("test", config.getAuthentication().getHost().getPath());
+    }
+}

+ 279 - 0
connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/AzureFetcherTest.java

@@ -0,0 +1,279 @@
+/*
+package cz.senslog.connector.fetch.azure;
+
+
+import cz.senslog.connector.config.model.HostConfig;
+import cz.senslog.connector.fetch.azure.auth.AzureAuthenticationService;
+import cz.senslog.connector.http.HttpClient;
+import cz.senslog.connector.http.HttpCode;
+import cz.senslog.connector.http.HttpRequest;
+import cz.senslog.connector.http.HttpResponse;
+import cz.senslog.connector.model.azure.AzureModel;
+import cz.senslog.connector.model.azure.SensorData;
+import cz.senslog.connector.model.azure.SensorInfo;
+import org.junit.jupiter.api.Test;
+import org.mockito.stubbing.Answer;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import static cz.senslog.connector.http.HttpCode.SERVER_ERROR;
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AzureFetcherTest {
+
+
+    @Test
+    void fetch_GetSensorsAndData_OneSensorOneData() throws Exception {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer((Answer<HttpResponse>) invocationOnMock -> {
+            HttpRequest request = invocationOnMock.getArgument(0, HttpRequest.class);
+            String path = request.getUrl().getPath();
+            if (path.equals("/sensorInfo")) {
+                SensorInfo sensorInfo = new SensorInfo();
+                sensorInfo.setEui("1234");
+                List<SensorInfo> bodyList = new ArrayList<>();
+                bodyList.add(sensorInfo);
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson(bodyList)).build();
+            } else if (path.equals("/sensorData")) {
+                List<SensorData> sensorDataList = new ArrayList<>();
+                SensorData data = new SensorData();
+                data.setTime(ZonedDateTime.of(LocalDateTime.MAX, ZoneOffset.UTC));
+                data.setTemperature(25.5F);
+                sensorDataList.add(data);
+                SensorInfo sensorInfo = new SensorInfo();
+                sensorInfo.setData(sensorDataList);
+                sensorInfo.setEui("1234");
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson(sensorInfo)).build();
+            }
+            return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+
+        });
+
+        AzureAuthenticationService authService = mock(AzureAuthenticationService.class);
+        when(authService.getAccessToken()).thenReturn("#12345");
+
+        AzureConfig config = mock(AzureConfig.class);
+        when(config.getStartDate()).thenReturn(LocalDateTime.MIN);
+        when(config.getSensorInfoHost()).thenReturn(new HostConfig("http://test.com", "sensorInfo"));
+        when(config.getSensorDataHost()).thenReturn(new HostConfig("http://test.com", "sensorData"));
+
+        AzureFetcher fetcher = new AzureFetcher(config, authService, httpClient);
+
+        fetcher.init();
+
+        AzureModel model = fetcher.fetch();
+
+        assertNotNull(model.getSensors());
+        assertEquals(LocalDateTime.MIN, model.getFrom());
+        assertEquals(LocalDateTime.MAX, model.getTo());
+        assertEquals(1, model.getSensors().size());
+
+        SensorInfo sensorInfo = model.getSensors().get(0);
+        assertEquals("1234", sensorInfo.getEui());
+        assertEquals(1, sensorInfo.getData().size());
+        assertEquals(25.5F, sensorInfo.getData().get(0).getTemperature());
+    }
+
+    @Test
+    void fetch_EmptySensors_EmptySensors() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer((Answer<HttpResponse>) invocationOnMock -> {
+            HttpRequest request = invocationOnMock.getArgument(0, HttpRequest.class);
+            String path = request.getUrl().getPath();
+            if (path.equals("/sensorInfo")) {
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson(new ArrayList<>())).build();
+            }
+            return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+
+        });
+
+        AzureAuthenticationService authService = mock(AzureAuthenticationService.class);
+        when(authService.getAccessToken()).thenReturn("#12345");
+
+        AzureConfig config = mock(AzureConfig.class);
+        when(config.getStartDate()).thenReturn(LocalDateTime.MIN);
+        when(config.getSensorInfoHost()).thenReturn(new HostConfig("http://test.com", "sensorInfo"));
+        when(config.getSensorDataHost()).thenReturn(new HostConfig("http://test.com", "sensorData"));
+
+        AzureFetcher fetcher = new AzureFetcher(config, authService, httpClient);
+
+        assertThrows(Exception.class, fetcher::init);
+
+        AzureModel model = fetcher.fetch();
+
+        assertNotNull(model.getSensors());
+        assertEquals(LocalDateTime.MIN, model.getFrom());
+        assertEquals(LocalDateTime.MIN, model.getTo());
+        assertEquals(0, model.getSensors().size());
+    }
+
+    @Test
+    void fetch_ErrorSensorsInfoRequest_EmptySensors() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer((Answer<HttpResponse>) invocationOnMock -> {
+            HttpRequest request = invocationOnMock.getArgument(0, HttpRequest.class);
+            String path = request.getUrl().getPath();
+            if (path.equals("/sensorInfo")) {
+                HttpResponse.newBuilder().status(SERVER_ERROR).build();
+            }
+            return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+
+        });
+
+        AzureAuthenticationService authService = mock(AzureAuthenticationService.class);
+        when(authService.getAccessToken()).thenReturn("#12345");
+
+        AzureConfig config = mock(AzureConfig.class);
+        when(config.getStartDate()).thenReturn(LocalDateTime.MIN);
+        when(config.getSensorInfoHost()).thenReturn(new HostConfig("http://test.com", "sensorInfo"));
+        when(config.getSensorDataHost()).thenReturn(new HostConfig("http://test.com", "sensorData"));
+
+        AzureFetcher fetcher = new AzureFetcher(config, authService, httpClient);
+
+        assertThrows(Exception.class, fetcher::init);
+
+        AzureModel model = fetcher.fetch();
+
+        assertNotNull(model.getSensors());
+        assertEquals(LocalDateTime.MIN, model.getFrom());
+        assertEquals(LocalDateTime.MIN, model.getTo());
+        assertEquals(0, model.getSensors().size());
+    }
+
+    @Test
+    void fetch_ErrorSensorDataRequest_OneSensorEmptyData() throws Exception {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer((Answer<HttpResponse>) invocationOnMock -> {
+            HttpRequest request = invocationOnMock.getArgument(0, HttpRequest.class);
+            String path = request.getUrl().getPath();
+            if (path.equals("/sensorInfo")) {
+                SensorInfo sensorInfo = new SensorInfo();
+                sensorInfo.setEui("1234");
+                List<SensorInfo> bodyList = new ArrayList<>();
+                bodyList.add(sensorInfo);
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson(bodyList)).build();
+            } else if (path.equals("/sensorData")) {
+                return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+            }
+            return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+
+        });
+
+        AzureAuthenticationService authService = mock(AzureAuthenticationService.class);
+        when(authService.getAccessToken()).thenReturn("#12345");
+
+        AzureConfig config = mock(AzureConfig.class);
+        when(config.getStartDate()).thenReturn(LocalDateTime.MIN);
+        when(config.getSensorInfoHost()).thenReturn(new HostConfig("http://test.com", "sensorInfo"));
+        when(config.getSensorDataHost()).thenReturn(new HostConfig("http://test.com", "sensorData"));
+
+        AzureFetcher fetcher = new AzureFetcher(config, authService, httpClient);
+
+        fetcher.init();
+
+        AzureModel model = fetcher.fetch();
+
+        assertNotNull(model.getSensors());
+        assertEquals(LocalDateTime.MIN, model.getFrom());
+        assertEquals(LocalDateTime.MIN, model.getTo());
+        assertEquals(1, model.getSensors().size());
+
+        SensorInfo sensorInfo = model.getSensors().get(0);
+        assertEquals("1234", sensorInfo.getEui());
+        assertEquals(0, sensorInfo.getData().size());
+    }
+
+    @Test
+    void fetch_InvalidJsonSensor_EmptySensors() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer((Answer<HttpResponse>) invocationOnMock -> {
+            HttpRequest request = invocationOnMock.getArgument(0, HttpRequest.class);
+            String path = request.getUrl().getPath();
+            if (path.equals("/sensorInfo")) {
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson("invalid_json")).build();
+            }
+            return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+        });
+
+        AzureAuthenticationService authService = mock(AzureAuthenticationService.class);
+        when(authService.getAccessToken()).thenReturn("#12345");
+
+        AzureConfig config = mock(AzureConfig.class);
+        when(config.getStartDate()).thenReturn(LocalDateTime.MIN);
+        when(config.getSensorInfoHost()).thenReturn(new HostConfig("http://test.com", "sensorInfo"));
+        when(config.getSensorDataHost()).thenReturn(new HostConfig("http://test.com", "sensorData"));
+
+        AzureFetcher fetcher = new AzureFetcher(config, authService, httpClient);
+
+        assertThrows(Exception.class, fetcher::init);
+
+        AzureModel model = fetcher.fetch();
+
+        assertNotNull(model.getSensors());
+        assertEquals(LocalDateTime.MIN, model.getFrom());
+        assertEquals(LocalDateTime.MIN, model.getTo());
+        assertEquals(0, model.getSensors().size());
+    }
+
+    @Test
+    void fetch_InvalidJsonData_OneSensorEmptyData() throws Exception {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer((Answer<HttpResponse>) invocationOnMock -> {
+            HttpRequest request = invocationOnMock.getArgument(0, HttpRequest.class);
+            String path = request.getUrl().getPath();
+            if (path.equals("/sensorInfo")) {
+                SensorInfo sensorInfo = new SensorInfo();
+                sensorInfo.setEui("1234");
+                List<SensorInfo> bodyList = new ArrayList<>();
+                bodyList.add(sensorInfo);
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson(bodyList)).build();
+            } else if (path.equals("/sensorData")) {
+                return HttpResponse.newBuilder().status(HttpCode.OK)
+                        .body(objectToJson("invalid_json")).build();
+            }
+            return HttpResponse.newBuilder().status(SERVER_ERROR).build();
+        });
+
+        AzureAuthenticationService authService = mock(AzureAuthenticationService.class);
+        when(authService.getAccessToken()).thenReturn("#12345");
+
+        AzureConfig config = mock(AzureConfig.class);
+        when(config.getStartDate()).thenReturn(LocalDateTime.MIN);
+        when(config.getSensorInfoHost()).thenReturn(new HostConfig("http://test.com", "sensorInfo"));
+        when(config.getSensorDataHost()).thenReturn(new HostConfig("http://test.com", "sensorData"));
+
+        AzureFetcher fetcher = new AzureFetcher(config, authService, httpClient);
+
+        fetcher.init();
+
+        AzureModel model = fetcher.fetch();
+
+        assertNotNull(model.getSensors());
+        assertEquals(LocalDateTime.MIN, model.getFrom());
+        assertEquals(LocalDateTime.MIN, model.getTo());
+        assertEquals(1, model.getSensors().size());
+
+        assertEquals(0, model.getSensors().get(0).getData().size());
+    }
+}
+ */

+ 46 - 0
connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/ConnectorFetchAzureProviderTest.java

@@ -0,0 +1,46 @@
+package cz.senslog.connector.fetch.azure;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+
+class ConnectorFetchAzureProviderTest {
+
+    @Test
+    void createFetcher_NewInstance_True() {
+
+        DefaultConfig defaultConfig = new DefaultConfig("azure", any());
+        defaultConfig.setProperty("startDate", LocalDateTime.MIN);
+        defaultConfig.setProperty("limitPerSensor", 42);
+        defaultConfig.setProperty("sensorInfoHost", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        defaultConfig.setProperty("sensorDataHost", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        defaultConfig.setProperty("authentication", new HashMap<String, Object>(){{
+            put("username", "username");
+            put("password", "password");
+            put("refreshPeriodIfFail", 100);
+            put("host", new HashMap<String, Object>(){{
+                put("domain", "http://test.com");
+                put("path", "test");
+            }});
+        }});
+
+        ConnectorFetchProvider provider = new ConnectorFetchAzureProvider();
+        ConnectorFetcher fetcher = provider.createFetcher(defaultConfig);
+
+        assertNotNull(fetcher);
+        assertEquals(AzureFetcher.class, fetcher.getClass());
+    }
+}

+ 281 - 0
connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/auth/AzureAuthenticationServiceTest.java

@@ -0,0 +1,281 @@
+package cz.senslog.connector.fetch.azure.auth;
+
+import cz.senslog.connector.config.model.DefaultConfig;
+import cz.senslog.connector.http.HttpClient;
+import cz.senslog.connector.http.HttpCode;
+import cz.senslog.connector.http.HttpRequest;
+import cz.senslog.connector.http.HttpResponse;
+import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AzureAuthenticationServiceTest {
+
+    @Test
+    void getAccessToken_Valid_Response_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                authInfo.setAccessToken("#12345");
+                List<AzureAuthorizationInfo> azureInfos = new ArrayList<>(1);
+                azureInfos.add(authInfo);
+                return HttpResponse.newBuilder()
+                        .status(HttpCode.OK).body(objectToJson(azureInfos)).build();
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+
+    @Test
+    void getAccessToken_EmptyResponse_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            private boolean invoked = false;
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                List<AzureAuthorizationInfo> body = new ArrayList<>();
+                if (invoked) {
+                    AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                    authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                    authInfo.setAccessToken("#12345");
+                    body.add(authInfo);
+                }
+                invoked = true;
+                return HttpResponse.newBuilder()
+                        .status(HttpCode.OK).body(objectToJson(body)).build();
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+
+    @Test
+    void getAccessToken_UnauthorizedResponse_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            private boolean invoked = false;
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                if (invoked) {
+                    List<AzureAuthorizationInfo> body = new ArrayList<>();
+                    AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                    authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                    authInfo.setAccessToken("#12345");
+                    body.add(authInfo);
+                    return HttpResponse.newBuilder()
+                            .status(HttpCode.OK).body(objectToJson(body)).build();
+                } else {
+                    invoked = true;
+                    Map<String, String> body = new HashMap<>();
+                    body.put("Message", "Unauthorized");
+                    return HttpResponse.newBuilder()
+                            .status(HttpCode.UNAUTHORIZED).body(objectToJson(body)).build();
+                }
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+
+    @Test
+    void getAccessToken_ErrorResponse_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            private boolean invoked = false;
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                if (invoked) {
+                    List<AzureAuthorizationInfo> body = new ArrayList<>();
+                    AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                    authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                    authInfo.setAccessToken("#12345");
+                    body.add(authInfo);
+                    return HttpResponse.newBuilder()
+                            .status(HttpCode.OK).body(objectToJson(body)).build();
+                } else {
+                    invoked = true;
+                    return HttpResponse.newBuilder()
+                            .status(HttpCode.SERVER_ERROR).body("Server Error").build();
+                }
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+
+    @Test
+    void getAccessToken_ExpiredToken_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            private boolean invoked = false;
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                List<AzureAuthorizationInfo> body = new ArrayList<>();
+                AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                authInfo.setAccessToken("#12345");
+                body.add(authInfo);
+                if (invoked) {
+                    authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                } else {
+                    invoked = true;
+                    authInfo.setExpires(LocalDateTime.now().minusDays(1));
+                }
+                return HttpResponse.newBuilder()
+                        .status(HttpCode.OK).body(objectToJson(body)).build();
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+
+    @Test
+    void getAccessToken_NotJsonResponse_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            private boolean invoked = false;
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                if (invoked) {
+                    List<AzureAuthorizationInfo> body = new ArrayList<>();
+                    AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                    authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                    authInfo.setAccessToken("#12345");
+                    body.add(authInfo);
+                    return HttpResponse.newBuilder()
+                            .status(HttpCode.OK).body(objectToJson(body)).build();
+                } else {
+                    invoked = true;
+                    return HttpResponse.newBuilder()
+                            .status(HttpCode.OK).body("invalid_json").build();
+                }
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+
+    @Test
+    void getAccessToken_BlankToken_True() {
+
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.send(any(HttpRequest.class))).thenAnswer(new Answer<HttpResponse>() {
+            private boolean invoked = false;
+            @Override public HttpResponse answer(InvocationOnMock invocationOnMock) {
+                List<AzureAuthorizationInfo> body = new ArrayList<>();
+                AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+                authInfo.setExpires(LocalDateTime.now().plusDays(1));
+                body.add(authInfo);
+                if (invoked) {
+                    authInfo.setAccessToken("#12345");
+                } else {
+                    invoked = true;
+                    authInfo.setAccessToken("   ");
+                }
+                return HttpResponse.newBuilder()
+                        .status(HttpCode.OK).body(objectToJson(body)).build();
+            }
+        });
+
+        DefaultConfig defaultConfig = new DefaultConfig("auth", any());
+        defaultConfig.setProperty("username", "username");
+        defaultConfig.setProperty("password", "password");
+        defaultConfig.setProperty("refreshPeriodIfFail", 100);
+        defaultConfig.setProperty("host", new HashMap<String, Object>(){{
+            put("domain", "http://test.com");
+            put("path", "test");
+        }});
+        AzureAuthConfig authConfig = new AzureAuthConfig(defaultConfig);
+
+        AzureAuthenticationService service = AzureAuthenticationService.newAuthService(authConfig, httpClient);
+
+        assertEquals("#12345", service.getAccessToken());
+    }
+}

+ 83 - 0
connector-fetch-azure/src/test/java/cz/senslog/connector/fetch/azure/auth/AzureAuthorizationInfoTest.java

@@ -0,0 +1,83 @@
+package cz.senslog.connector.fetch.azure.auth;
+
+import cz.senslog.connector.json.BasicJson;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AzureAuthorizationInfoTest {
+
+    @Test
+    void isAuthorized_ValidExpires_True(){
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+        authInfo.setAccessToken("#12345");
+        authInfo.setExpires(LocalDateTime.now().plusMinutes(1));
+
+        assertTrue(authInfo.isAuthorized());
+    }
+
+    @Test
+    void isAuthorized_InvalidExpires_False() {
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+        authInfo.setAccessToken("#12345");
+        authInfo.setExpires(LocalDateTime.now().minusMinutes(1));
+
+        assertFalse(authInfo.isAuthorized());
+    }
+
+    @Test
+    void isAuthorized_MissingExpires_False() {
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+        authInfo.setAccessToken("#12345");
+
+        assertFalse(authInfo.isAuthorized());
+    }
+
+    @Test
+    void isAuthorized_MissingToken_False() {
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+        authInfo.setExpires(LocalDateTime.now().plusMinutes(1));
+
+        assertFalse(authInfo.isAuthorized());
+    }
+
+    @Test
+    void isAuthorized_MissingAll_False() {
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+
+        assertFalse(authInfo.isAuthorized());
+    }
+
+    @Test
+    void getAccessTokenAndExpires_True() {
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+        authInfo.setAccessToken("#12345");
+        authInfo.setExpires(LocalDateTime.MIN);
+
+        assertEquals("#12345", authInfo.getAccessToken());
+        assertEquals(LocalDateTime.MIN, authInfo.getExpires());
+    }
+
+    @Test
+    void toString_True() {
+
+        AzureAuthorizationInfo authInfo = new AzureAuthorizationInfo();
+        authInfo.setAccessToken("#12345");
+        authInfo.setExpires(LocalDateTime.MIN);
+
+        AzureAuthorizationInfo parsedInfo = BasicJson.jsonToObject(authInfo.toString(), AzureAuthorizationInfo.class);
+
+        assertEquals("#12345", parsedInfo.getAccessToken());
+        assertEquals(LocalDateTime.MIN, parsedInfo.getExpires());
+
+
+    }
+}

+ 76 - 0
connector-fetch-azure/src/test/resources/schema/sensorDataSchema.json

@@ -0,0 +1,76 @@
+{
+  "definitions": {},
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://iotlorawan.azurewebsites.net/sensorInfo.json",
+  "type": "object",
+  "title": "Detailed sensor data",
+  "required": [
+    "eui",
+    "name",
+    "data"
+  ],
+  "properties": {
+    "eui": {
+      "$id": "#/properties/eui",
+      "type": "string",
+      "title": "Eui",
+      "pattern": "^[0-9a-fA-F]+"
+    },
+    "name": {
+      "$id": "#/properties/name",
+      "type": "string",
+      "title": "Name",
+      "pattern": "^(.*)$"
+    },
+    "data": {
+      "$id": "#/properties/data",
+      "type": "array",
+      "title": "The Data Schema",
+      "items": {
+        "$id": "#/properties/data/items",
+        "type": "object",
+        "title": "The Items Schema",
+        "properties": {
+          "time": {
+            "$id": "#/properties/data/items/properties/time",
+            "type": "string",
+            "title": "Time",
+            "description": "Date time when data was measured.",
+            "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})[+](?<zone>[0-9]{2}[:][0-9]{2})"
+          },
+          "temperature": {
+            "$id": "#/properties/data/items/properties/temperature",
+            "type": ["number", "null"],
+            "title": "Temperature"
+          },
+          "humidity": {
+            "$id": "#/properties/data/items/properties/humidity",
+            "type": ["number", "null"],
+            "title": "Humidity"
+          },
+          "co2": {
+            "$id": "#/properties/data/items/properties/co2",
+            "type": ["integer", "null"],
+            "title": "Co2"
+          },
+          "rssi": {
+            "$id": "#/properties/data/items/properties/rssi",
+            "type": ["integer", "null"],
+            "title": "Rssi"
+          },
+          "snr": {
+            "$id": "#/properties/data/items/properties/snr",
+            "type": ["number", "null"],
+            "title": "Snr"
+          },
+          "batteryLevel": {
+            "$id": "#/properties/data/items/properties/batteryLevel",
+            "type": ["number", "null"],
+            "title": "Battery level",
+            "minimum": 0
+          }
+        }
+      }
+    }
+  }
+}

+ 106 - 0
connector-fetch-azure/src/test/resources/schema/sensorInfoSchema.json

@@ -0,0 +1,106 @@
+{
+  "definitions": {},
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://iotlorawan.azurewebsites.net/sensorInfo.json",
+  "type": "array",
+  "title": "List of available sensors",
+  "default": null,
+  "items": {
+    "$id": "#/items",
+    "type": "object",
+    "title": "Sensor info",
+    "description": "Full description of each sensor",
+    "required": [
+      "eui",
+      "acronym",
+      "name",
+      "period",
+      "tolerance",
+      "location",
+      "description",
+      "sensorDataId",
+      "sensorDataTime",
+      "created",
+      "status"
+    ],
+    "properties": {
+      "eui": {
+        "$id": "#/items/properties/eui",
+        "type": "string",
+        "title": "Eui",
+        "pattern": "^[0-9a-fA-F]+"
+      },
+      "acronym": {
+        "$id": "#/items/properties/acronym",
+        "type": "string",
+        "title": "Acronym",
+        "pattern": "^[A-Z]+-[0-9]+"
+      },
+      "name": {
+        "$id": "#/items/properties/name",
+        "type": "string",
+        "title": "Name",
+        "pattern": "^(.*)$"
+      },
+      "period": {
+        "$id": "#/items/properties/period",
+        "type": "integer",
+        "title": "Period",
+        "minimum": 0.0
+      },
+      "tolerance": {
+        "$id": "#/items/properties/tolerance",
+        "type": "integer",
+        "title": "Tolerance"
+      },
+      "location": {
+        "$id": "#/items/properties/location",
+        "type": "string",
+        "title": "Location",
+        "description": "String location of the sensor.",
+        "default": "unknown",
+        "pattern": "^(.*)$"
+      },
+      "description": {
+        "$id": "#/items/properties/description",
+        "type": "string",
+        "title": "Sensor description",
+        "default": "",
+        "pattern": "^(.*)$"
+      },
+      "sensorDataId": {
+        "$id": "#/items/properties/sensorDataId",
+        "type": "integer",
+        "title": "Identifier of the sensor"
+      },
+      "sensorDataTime": {
+        "$id": "#/items/properties/sensorDataTime",
+        "type": "string",
+        "title": "Sensor Data time",
+        "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(.(?<millisecond>[0-9]+))?"
+      },
+      "created": {
+        "$id": "#/items/properties/created",
+        "type": ["string","null"],
+        "title": "Created date time",
+        "description": "Date when the sensor was connected.",
+        "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(.(?<millisecond>[0-9]+))?"
+      },
+      "deleted": {
+        "$id": "#/items/properties/deleted",
+        "type": ["string","null"],
+        "title": "Deleted date time",
+        "description": "Date when the sensor was disconnected.",
+        "pattern": "^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})T(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2})(.(?<millisecond>[0-9]+))?"
+      },
+      "status": {
+        "$id": "#/items/properties/status",
+        "type": "integer",
+        "title": "Status of the sensor",
+        "description": "Signalizes state of the sensor: 1 - active, 0 - inactive.",
+        "minimum": 0.0,
+        "maximum": 1.0
+      }
+    }
+  }
+}

+ 22 - 0
connector-model/pom.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>connectors</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-model</artifactId>
+    <version>${project.parent.version}</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-common</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 30 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/AbstractModel.java

@@ -0,0 +1,30 @@
+package cz.senslog.connector.model.api;
+
+import java.time.LocalDateTime;
+
+/**
+ * The abstract class {@code AbstractModel} represents a base class
+ * for all models which want to be used as a transfer model for a connector.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public abstract class AbstractModel {
+
+    /** Time range from until were gotten the data. */
+    private final LocalDateTime from, to;
+
+    protected AbstractModel(LocalDateTime from, LocalDateTime to) {
+        this.from = from;
+        this.to = to;
+    }
+
+    public LocalDateTime getFrom() {
+        return from;
+    }
+
+    public LocalDateTime getTo() {
+        return to;
+    }
+}

+ 24 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/Converter.java

@@ -0,0 +1,24 @@
+package cz.senslog.connector.model.api;
+
+/**
+ * The interface {@code Converter} provides a generic functionality
+ * for converter. Each class which implements which interface can be registered
+ * as a converter for a connector. For converter can be used only classes which extend {@link AbstractModel}.
+ *
+ *
+ * @param <IN> which type of class will be used as an input of a converter.
+ * @param <OUT> which type of class will be used as an output of a converter.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Converter<IN extends AbstractModel, OUT extends AbstractModel> {
+
+    /**
+     * Provides an interface for converting from input to output model.
+     * @param model - model which is converted to output.
+     * @return converted input model.
+     */
+    OUT convert(IN model);
+}

+ 63 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/ConverterProvider.java

@@ -0,0 +1,63 @@
+package cz.senslog.connector.model.api;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class ConverterProvider {
+
+    private static Logger logger = LogManager.getLogger(ConverterProvider.class);
+
+    private static class Wrapper {
+        private final Class input;
+        private final Class output;
+        private final Converter converter;
+        private Wrapper(Class input, Class output, Converter converter) {
+            this.input = input;
+            this.output = output;
+            this.converter = converter;
+        }
+    }
+
+    private final List<Wrapper> CONVERTERS = new ArrayList<>();
+
+    protected ConverterProvider() {
+        config();
+    }
+
+    protected abstract void config();
+
+    protected void register(Class<? extends Converter> converterClass) {
+        logger.debug("Registering a new converter {}", converterClass);
+        try {
+            logger.debug("Getting a generic parameters from the class {}.", converterClass);
+            ParameterizedType converterTypes = (ParameterizedType) converterClass.getGenericInterfaces()[0];
+            Class fetchModel = (Class) converterTypes.getActualTypeArguments()[0];
+            Class pushModel = (Class) converterTypes.getActualTypeArguments()[1];
+
+            logger.debug("Creating a new instance of the class {}.", converterClass);
+            Converter converter = converterClass.newInstance();
+
+            CONVERTERS.add(new Wrapper(fetchModel, pushModel, converter));
+            logger.info("Registered a new converter {} for {} -> {}.", converterClass, fetchModel, pushModel);
+        } catch (InstantiationException | IllegalAccessException e) {
+            logger.error("Can not create an instance for {}.", converterClass);
+            logger.catching(e);
+        }
+
+    }
+
+    public Converter getConverter(Class<? extends AbstractModel> fetchModel, Class<? extends AbstractModel> pushModel) {
+        logger.info("Getting a converter for {} -> {}. ", fetchModel, pushModel);
+        for (Wrapper item : CONVERTERS) {
+            if (item.input.equals(fetchModel) && item.output.equals(pushModel)) {
+                return item.converter;
+            }
+        }
+        logger.warn("The converter for {} -> {} was not found.", fetchModel, pushModel);
+        return null;
+    }
+}

+ 35 - 0
connector-model/src/main/java/cz/senslog/connector/model/azure/AzureModel.java

@@ -0,0 +1,35 @@
+package cz.senslog.connector.model.azure;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * The class {@code AzureModel} represents a model which contains
+ * transfer data from Azure IoT LoraWan.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AzureModel extends AbstractModel {
+
+    /** List of sensor data */
+    private final List<SensorInfo> sensors;
+
+    /**
+     * Constructor of the class sets all attributes.
+     * @param sensors - list of the sensors.
+     * @param from - start of the time range.
+     * @param to - end of the time range.
+     */
+    public AzureModel(List<SensorInfo> sensors, LocalDateTime from, LocalDateTime to) {
+        super(from, to);
+        this.sensors = sensors;
+    }
+
+    public List<SensorInfo> getSensors() {
+        return sensors;
+    }
+}

+ 114 - 0
connector-model/src/main/java/cz/senslog/connector/model/azure/SensorData.java

@@ -0,0 +1,114 @@
+package cz.senslog.connector.model.azure;
+
+import cz.senslog.connector.util.NumberUtils;
+import org.apache.logging.log4j.util.BiConsumer;
+
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cz.senslog.connector.model.azure.SensorType.*;
+
+/**
+ * The class {@code SensorData} represents transfer object of measure
+ * data from each sensor depends on time.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class SensorData {
+
+    /** Timestamp when values were measured. */
+    private ZonedDateTime time;
+
+    /** Value of temperature in celsius degree (°C). */
+    private Float temperature;
+
+    /** Value of humidity. */
+    private Float humidity;
+
+    /** Value of carbon dioxide (CO2). */
+    private Float co2;
+
+    /** Received signal strength indication (RSSI). */
+    private Integer rssi;
+
+    /** Signal to noise ratio (SNR). */
+    private Float snr;
+
+    /** Value of battery level. */
+    private Float batteryLevel;
+
+    public ZonedDateTime getTime() {
+        return time;
+    }
+
+    public void setTime(ZonedDateTime time) {
+        this.time = time;
+    }
+
+    public Float getTemperature() {
+        return temperature;
+    }
+
+    public void setTemperature(Float temperature) {
+        this.temperature = temperature;
+    }
+
+    public Float getHumidity() {
+        return humidity;
+    }
+
+    public void setHumidity(Float humidity) {
+        this.humidity = humidity;
+    }
+
+    public Float getCo2() {
+        return co2;
+    }
+
+    public void setCo2(Float co2) {
+        this.co2 = co2;
+    }
+
+    public Integer getRssi() {
+        return rssi;
+    }
+
+    public void setRssi(Integer rssi) {
+        this.rssi = rssi;
+    }
+
+    public Float getSnr() {
+        return snr;
+    }
+
+    public void setSnr(Float snr) {
+        this.snr = snr;
+    }
+
+    public Float getBatteryLevel() {
+        return batteryLevel;
+    }
+
+    public void setBatteryLevel(Float batteryLevel) {
+        this.batteryLevel = batteryLevel;
+    }
+
+    public Map<SensorType, Float> getValues() {
+        Map<SensorType, Float> values = new HashMap<>();
+        BiConsumer<SensorType, Float> setValue = (type, value) -> {
+          if (value != null) values.put(type, value);
+        };
+
+        setValue.accept(BATTERY_LEVEL, batteryLevel);
+        setValue.accept(SNR, snr);
+        setValue.accept(RSSI, NumberUtils.valueOf(rssi));
+        setValue.accept(CO2, co2);
+        setValue.accept(HUMIDITY, humidity);
+        setValue.accept(TEMPERATURE, temperature);
+
+        return values;
+    }
+}

+ 170 - 0
connector-model/src/main/java/cz/senslog/connector/model/azure/SensorInfo.java

@@ -0,0 +1,170 @@
+package cz.senslog.connector.model.azure;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cz.senslog.connector.json.BasicJson.objectToJson;
+import static java.lang.Boolean.TRUE;
+import static java.util.Collections.emptyList;
+
+/**
+ * The class {@code SensorInfo} represents transfer object of sensors.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class SensorInfo {
+
+    /** Identifier of sensor. */
+    private String eui;
+
+    /** Name of sensor. */
+    private String name;
+
+    /** Acronym of sensor. */
+    private String acronym;
+
+    /**  */
+    private Integer period;
+
+    /**  */
+    private Integer tolerance;
+
+    /** Place where the sensor is located. */
+    private String location;
+
+    /** Description of what sensor does. */
+    private String description;
+
+    /** */
+    private Long sensorDataId;
+
+    /**  */
+    private LocalDateTime sensorDataTime;
+
+    /** Time when sensor was created and started to produce data. */
+    private LocalDateTime created;
+
+    /** Time when sensor was deleted and stopped to produce data. */
+    private LocalDateTime deleted;
+
+    /** Status of sensor: 0 - inactive, 1 - active. */
+    private Integer status;
+
+    /** List of measured data */
+    private List<SensorData> data = emptyList();
+
+    public String getEui() {
+        return eui;
+    }
+
+    public void setEui(String eui) {
+        this.eui = eui;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getAcronym() {
+        return acronym;
+    }
+
+    public void setAcronym(String acronym) {
+        this.acronym = acronym;
+    }
+
+    public Integer getPeriod() {
+        return period;
+    }
+
+    public void setPeriod(Integer period) {
+        this.period = period;
+    }
+
+    public Integer getTolerance() {
+        return tolerance;
+    }
+
+    public void setTolerance(Integer tolerance) {
+        this.tolerance = tolerance;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public void setLocation(String location) {
+        this.location = location;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Long getSensorDataId() {
+        return sensorDataId;
+    }
+
+    public void setSensorDataId(Long sensorDataId) {
+        this.sensorDataId = sensorDataId;
+    }
+
+    public LocalDateTime getSensorDataTime() {
+        return sensorDataTime;
+    }
+
+    public void setSensorDataTime(LocalDateTime sensorDataTime) {
+        this.sensorDataTime = sensorDataTime;
+    }
+
+    public LocalDateTime getCreated() {
+        return created;
+    }
+
+    public void setCreated(LocalDateTime created) {
+        this.created = created;
+    }
+
+    public LocalDateTime getDeleted() {
+        return deleted;
+    }
+
+    public void setDeleted(LocalDateTime deleted) {
+        this.deleted = deleted;
+    }
+
+    public Boolean getStatus() {
+        return status != null && status > 0;
+    }
+
+    public boolean isActive() {
+        return getStatus().equals(TRUE);
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    public List<SensorData> getData() {
+        return data;
+    }
+
+    public void setData(List<SensorData> data) {
+        this.data = data;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 34 - 0
connector-model/src/main/java/cz/senslog/connector/model/azure/SensorType.java

@@ -0,0 +1,34 @@
+package cz.senslog.connector.model.azure;
+
+/**
+ * The enum {@code SensorType} represents type of sensor
+ * which each physical quantity is represents by identifier.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public enum SensorType {
+
+    TEMPERATURE     (340020000),
+    HUMIDITY        (410010000),
+    CO2             (780020000),
+    RSSI            (380010000),
+    SNR             (2L),    // TODO fill
+    BATTERY_LEVEL   (560030000),
+
+    ;
+    private final long id;
+
+    SensorType(long id) {
+        this.id = id;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public static int count() {
+        return values().length;
+    }
+}

+ 96 - 0
connector-model/src/main/java/cz/senslog/connector/model/converter/AzureModelSenslogV1ModelConverter.java

@@ -0,0 +1,96 @@
+package cz.senslog.connector.model.converter;
+
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.azure.AzureModel;
+import cz.senslog.connector.model.azure.SensorData;
+import cz.senslog.connector.model.azure.SensorInfo;
+import cz.senslog.connector.model.azure.SensorType;
+import cz.senslog.connector.model.v1.Observation;
+import cz.senslog.connector.model.v1.SenslogV1Model;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The class {@code AzureModelSenslogV1ModelConverter} represents an implementation of converter
+ * between {@link AzureModel} and {@link SenslogV1Model}.
+ *
+ * Converter converts input model of sensors data to observations. Each physical quantity of sensor
+ * is converted as a single observation.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class AzureModelSenslogV1ModelConverter implements Converter<AzureModel, SenslogV1Model> {
+
+    private static Logger logger = LogManager.getLogger(AzureModelSenslogV1ModelConverter.class);
+
+    /**
+     * Converter sensor EUI in hex value to sensor ID in dec value.
+     * @param hexValue - identifier in hex.
+     * @return identifier in dec.
+     */
+    private static Long convertEuiHexToUnitIdDec(String hexValue) {
+        switch (hexValue) {
+            case "8CF9574000000948": return 10002376L;
+            case "8CF95740000008AE": return 10002222L;
+            default: return null;
+        }
+    }
+
+    @Override
+    public SenslogV1Model convert(AzureModel inputModel) {
+
+        if (inputModel == null || inputModel.getSensors() == null) {
+            logger.warn("Nothing to convert. Received model is empty.");
+            return null;
+        }
+
+        List<SensorInfo> sensorInfos = inputModel.getSensors();
+
+        int size = 0, records = 0;
+        for (SensorInfo sensorInfo : sensorInfos) {
+            records += sensorInfo.getData().size();
+            size += sensorInfo.getData().size() * SensorType.count();
+        }
+        logger.info("Received {} sensors and {} records to convert.", sensorInfos.size(), records);
+
+        logger.debug("Creating an empty list of observation with init size {}.", size);
+        List<Observation> observations = new ArrayList<>(size);
+
+        for (SensorInfo sensorInfo : sensorInfos) {
+            logger.debug("Converting sensor with Eui {}.", sensorInfo.getEui());
+
+            Long unitId = convertEuiHexToUnitIdDec(sensorInfo.getEui());
+            if (unitId == null) {
+                logger.error("EUI converter for '{}' does not exist.", sensorInfo.getEui());
+                continue;
+            }
+
+            for (SensorData sensorData : sensorInfo.getData()) {
+                for (Map.Entry<SensorType, Float> sensorDataEntry : sensorData.getValues().entrySet()) {
+                    logger.debug("Creating a new observation.");
+                    Observation observation = new Observation();
+                    observation.setUnitId(unitId);
+                    observation.setTime(sensorData.getTime());
+                    observation.setValue(sensorDataEntry.getValue());
+                    observation.setSensorId(sensorDataEntry.getKey().getId());
+
+                    logger.debug("Saving the new observation.");
+                    observations.add(observation);
+                }
+            }
+        }
+
+        logger.debug("Creating a new SenslogV1 model.");
+        SenslogV1Model outputModel = new SenslogV1Model(observations, inputModel.getFrom(), inputModel.getTo());
+
+        logger.info("Conversion was completed successfully.");
+
+        return outputModel;
+    }
+}

部分文件因为文件数量过多而无法显示