MSI\matet 5 роки тому
коміт
73e9c7e0a4
42 змінених файлів з 1756 додано та 0 видалено
  1. 49 0
      build.gradle
  2. 38 0
      config/default.yaml
  3. 19 0
      config/email.txt
  4. BIN
      gradle/wrapper/gradle-wrapper.jar
  5. 5 0
      gradle/wrapper/gradle-wrapper.properties
  6. 104 0
      gradlew.bat
  7. 2 0
      settings.gradle
  8. 62 0
      src/main/java/cz/senslog/watchdog/app/Application.java
  9. 10 0
      src/main/java/cz/senslog/watchdog/app/Main.java
  10. 78 0
      src/main/java/cz/senslog/watchdog/app/Parameters.java
  11. 56 0
      src/main/java/cz/senslog/watchdog/app/Watcher.java
  12. 101 0
      src/main/java/cz/senslog/watchdog/config/Configuration.java
  13. 25 0
      src/main/java/cz/senslog/watchdog/config/DataProviderConfig.java
  14. 10 0
      src/main/java/cz/senslog/watchdog/config/DataProviderType.java
  15. 49 0
      src/main/java/cz/senslog/watchdog/config/DatabaseConfig.java
  16. 63 0
      src/main/java/cz/senslog/watchdog/config/EmailMessageBrokerConfig.java
  17. 26 0
      src/main/java/cz/senslog/watchdog/config/MessageBrokerConfig.java
  18. 11 0
      src/main/java/cz/senslog/watchdog/config/MessageBrokerType.java
  19. 165 0
      src/main/java/cz/senslog/watchdog/config/MonitoredObjectsConfig.java
  20. 189 0
      src/main/java/cz/senslog/watchdog/config/PropertyConfig.java
  21. 6 0
      src/main/java/cz/senslog/watchdog/messagebroker/BlockingMessageBroker.java
  22. 21 0
      src/main/java/cz/senslog/watchdog/messagebroker/ConsoleMessageBroker.java
  23. 95 0
      src/main/java/cz/senslog/watchdog/messagebroker/EmailMessageBroker.java
  24. 22 0
      src/main/java/cz/senslog/watchdog/messagebroker/MessageBroker.java
  25. 6 0
      src/main/java/cz/senslog/watchdog/messagebroker/MessageBrokerHandler.java
  26. 33 0
      src/main/java/cz/senslog/watchdog/messagebroker/MessageStatus.java
  27. 21 0
      src/main/java/cz/senslog/watchdog/provider/DataProvider.java
  28. 13 0
      src/main/java/cz/senslog/watchdog/provider/Record.java
  29. 38 0
      src/main/java/cz/senslog/watchdog/provider/database/Connection.java
  30. 27 0
      src/main/java/cz/senslog/watchdog/provider/database/DatabaseDataProvider.java
  31. 55 0
      src/main/java/cz/senslog/watchdog/provider/database/ObservationInfo.java
  32. 47 0
      src/main/java/cz/senslog/watchdog/provider/database/SensLogRepository.java
  33. 15 0
      src/main/java/cz/senslog/watchdog/provider/ws/WebServiceDataProvider.java
  34. 22 0
      src/main/java/cz/senslog/watchdog/util/StringUtils.java
  35. 40 0
      src/main/java/cz/senslog/watchdog/util/Tuple.java
  36. 50 0
      src/main/java/cz/senslog/watchdog/util/schedule/ScheduleTask.java
  37. 24 0
      src/main/java/cz/senslog/watchdog/util/schedule/Scheduler.java
  38. 30 0
      src/main/java/cz/senslog/watchdog/util/schedule/SchedulerBuilderImpl.java
  39. 60 0
      src/main/java/cz/senslog/watchdog/util/schedule/SchedulerImpl.java
  40. 5 0
      src/main/java/cz/senslog/watchdog/util/schedule/Status.java
  41. 29 0
      src/main/java/cz/senslog/watchdog/util/schedule/TaskDescription.java
  42. 35 0
      src/main/resources/log4j2.xml

+ 49 - 0
build.gradle

@@ -0,0 +1,49 @@
+plugins {
+    id 'java'
+}
+
+group 'cz.senslog'
+version '1.0-SNAPSHOT'
+
+repositories {
+    mavenLocal()
+    mavenCentral()
+}
+
+test {
+    useJUnitPlatform()
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+jar {
+    manifest {
+        attributes(
+                'Main-Class': 'cz.senslog.watchdog.app.Main'
+        )
+    }
+    from {
+        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+    }
+}
+
+dependencies {
+
+    compile group: 'com.beust', name: 'jcommander', version: '1.78'
+    compile group: 'org.yaml', name: 'snakeyaml', version: '1.24'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.12.0'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.12.0'
+
+    compile group: 'org.jdbi', name: 'jdbi3-postgres', version: '3.12.2'
+    compile group: 'org.jdbi', name: 'jdbi3-jodatime2', version: '3.12.2'
+    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.4.2'
+    compile group: 'org.postgresql', name: 'postgresql', version: '42.2.10'
+
+    compile group: 'javax.mail', name: 'javax.mail-api', version: '1.6.2'
+    compile group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2'
+
+    testCompile group: 'junit', name: 'junit', version: '4.12'
+}

+ 38 - 0
config/default.yaml

@@ -0,0 +1,38 @@
+dataProvider:
+  type: DATABASE
+  config:
+    url: "jdbc:postgresql://localhost:5432/senslog1"
+    username: "postgres"
+    password: "root"
+    connectionPoolSize: 6
+    groupName: "admin"
+
+messageBroker:
+  type: EMAIL # EMAIL, CONSOLE (for testing), WHATSAPP (not yet), TELEGRAM (not yet)
+  config:
+    smtpHost: "mail.lesprojekt.cz"
+    smtpPort: 465
+    authUsername: "watchdog@senslog.org"
+    authPassword: "5jspdD"
+    senderEmail: "watchdog@senslog.org"
+    recipientEmail: "watchdog@senslog.org"
+    subject: "[Alert] Watchdog SensLog"
+
+monitoredObjects:
+  18907677:
+    interval: 1440
+
+  4280003:
+    interval: 1440
+    sensors: [120100000, 410170000]
+
+  4638425647:
+    interval: 1440
+    sensors:
+      850010000:
+      850020000: 2880
+
+  18907678:
+    sensors:
+      800014061: 2880
+      800014060: 1440

+ 19 - 0
config/email.txt

@@ -0,0 +1,19 @@
+WatchDog email
+watchdog@senslog.org
+5jspdD
+server: mail.lesprojekt.cz
+
+odchozí
+server: mail.lesprojekt.cz
+port: 465
+autentizace: SSL/TLS
+způsob: heslo, zabezpečený
+
+příchozí
+mail.lesprojekt.cz
+port: 993
+autentizace: SSL/TLS
+způsob: heslo, zabezpečený
+
+web client
+https://webmail.lesprojekt.cz/

BIN
gradle/wrapper/gradle-wrapper.jar


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

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 104 - 0
gradlew.bat

@@ -0,0 +1,104 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 2 - 0
settings.gradle

@@ -0,0 +1,2 @@
+rootProject.name = 'watchdog'
+

+ 62 - 0
src/main/java/cz/senslog/watchdog/app/Application.java

@@ -0,0 +1,62 @@
+package cz.senslog.watchdog.app;
+
+
+import cz.senslog.watchdog.config.Configuration;
+import cz.senslog.watchdog.messagebroker.MessageBroker;
+import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.util.schedule.Scheduler;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+
+public class Application extends Thread {
+
+    private static final Logger logger = LogManager.getLogger(Application.class);
+
+    private final Parameters params;
+
+    static Thread init(String... args) throws IOException {
+        Parameters parameters = Parameters.parse(args);
+
+        if (parameters.isHelp()) {
+            return new Thread(parameters::printHelp);
+        }
+
+        Application app = new Application(parameters);
+        Runtime.getRuntime().addShutdownHook(new Thread(app::interrupt, "clean-app"));
+
+        return app;
+    }
+
+    private Application(Parameters parameters) {
+        super("app");
+        this.params = parameters;
+    }
+
+    @Override
+    public void interrupt() {}
+
+    @Override
+    public void run() {
+
+        String configFile = params.getConfigFileName();
+        Configuration config = null;
+        try {
+            config = Configuration.load(configFile);
+        } catch (IOException e) {
+            System.exit(1);
+        }
+
+        DataProvider dataProvider = DataProvider.create(config.getDataProviderConfig());
+        MessageBroker messageBroker = MessageBroker.create(config.getMessageBrokerConfig());
+        Watcher watcher = Watcher.create(config.getWatchingObjectsConfig(), dataProvider, messageBroker);
+
+        long period = config.getWatchingObjectsConfig().getMinInterval();
+        Scheduler scheduler = Scheduler.createBuilder()
+                    .addTask(watcher::check, period)
+                .build();
+
+        scheduler.start();
+    }
+}

+ 10 - 0
src/main/java/cz/senslog/watchdog/app/Main.java

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

+ 78 - 0
src/main/java/cz/senslog/watchdog/app/Parameters.java

@@ -0,0 +1,78 @@
+package cz.senslog.watchdog.app;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static cz.senslog.watchdog.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 final Logger logger = LogManager.getLogger(Parameters.class);
+
+    private JCommander jCommander;
+
+    /**
+     * 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 jCommander = JCommander.newBuilder()
+                .addObject(parameters).build();
+        parameters.jCommander = jCommander;
+
+        jCommander.parse(args);
+
+        String configFileName = parameters.getConfigFileName();
+        logger.debug("Checking existence of configuration file {}", configFileName);
+        if (isNotBlank(configFileName) && notExists(get(configFileName))) {
+            throw new FileNotFoundException(format("Config file %s does not exist.", configFileName));
+        }
+
+        logger.info("Parsing input parameters {} were parsed successfully.", Arrays.toString(args));
+        return parameters;
+    }
+
+    @Parameter(names = {"-h", "-help"}, help = true)
+    private boolean help = false;
+
+    @Parameter(names = {"-cf", "-config-file"}, description = "Configuration file in .yaml format.")
+    private String configFileName;
+
+    /**
+     * Returns name of the configuration file.
+     * @return string name.
+     */
+    public String getConfigFileName() {
+        return configFileName;
+    }
+
+    public boolean isHelp() {
+        return help;
+    }
+
+    public void printHelp() {
+        jCommander.usage();
+    }
+}

+ 56 - 0
src/main/java/cz/senslog/watchdog/app/Watcher.java

@@ -0,0 +1,56 @@
+package cz.senslog.watchdog.app;
+
+import cz.senslog.watchdog.config.MonitoredObjectsConfig;
+import cz.senslog.watchdog.messagebroker.MessageBroker;
+import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.provider.Record;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+
+import static java.lang.String.format;
+
+public class Watcher {
+
+    private static final Logger logger = LogManager.getLogger(Watcher.class);
+
+    private final MonitoredObjectsConfig config;
+    private final DataProvider dataProvider;
+    private final MessageBroker messageBroker;
+
+    public static Watcher create(MonitoredObjectsConfig config, DataProvider dataProvider, MessageBroker messageBroker) {
+        return new Watcher(config, dataProvider, messageBroker);
+    }
+
+    private Watcher(MonitoredObjectsConfig config, DataProvider dataProvider, MessageBroker messageBroker) {
+        this.config = config;
+        this.dataProvider = dataProvider;
+        this.messageBroker = messageBroker;
+    }
+
+    public void check() {
+        Instant now = Instant.now();
+        List<Record> records = dataProvider.getLastRecords();
+        for (Record record : records) {
+            Optional<Long> intervalOpt = config.getInterval(record);
+            if (intervalOpt.isPresent() && !record.isValid(intervalOpt.get(), now)) {
+                String message = format("The monitored object '%s' does not contain a new data.", record);
+                messageBroker.send(message, status -> {
+                    String brokerType = messageBroker.getType().name().toLowerCase();
+                    if (status.isSuccess()) {
+                        logger.info("The message '{}' was send via '{}' successfully.",
+                                status.getMessage(), brokerType
+                        );
+                    } else {
+                        logger.error("Can not send a message '{}' via '{}' because of '{}'.",
+                                status.getMessage(), brokerType, status.getError()
+                        );
+                    }
+                });
+            }
+        }
+    }
+}

+ 101 - 0
src/main/java/cz/senslog/watchdog/config/Configuration.java

@@ -0,0 +1,101 @@
+package cz.senslog.watchdog.config;
+
+
+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.Map;
+
+public class Configuration {
+
+    private static final Logger logger = LogManager.getLogger(Configuration.class);
+
+    private final DataProviderConfig dataProviderConfig;
+    private final MessageBrokerConfig messageBrokerConfig;
+    private final MonitoredObjectsConfig monitoredObjectsConfig;
+
+    private Configuration(DataProviderConfig dataProvider, MessageBrokerConfig messageBroker, MonitoredObjectsConfig monitoredObjects) {
+        this.dataProviderConfig = dataProvider;
+        this.messageBrokerConfig = messageBroker;
+        this.monitoredObjectsConfig = monitoredObjects;
+    }
+
+    public DataProviderConfig getDataProviderConfig() {
+        return dataProviderConfig;
+    }
+
+    public MessageBrokerConfig getMessageBrokerConfig() {
+        return messageBrokerConfig;
+    }
+
+    public MonitoredObjectsConfig getWatchingObjectsConfig() {
+        return monitoredObjectsConfig;
+    }
+
+    public static Configuration load(String fileName) throws IOException {
+
+        logger.info("Loading '{}' configuration file.", fileName);
+
+        if (!fileName.toLowerCase().endsWith(".yaml")) {
+            throw new IllegalArgumentException(fileName + "does not contain .yaml extension.");
+        }
+
+        Path filePath = Paths.get(fileName);
+        if (Files.notExists(filePath)) {
+            throw new FileNotFoundException(fileName + " does not exist");
+        }
+
+        Map<Object, Object> properties;
+
+        logger.debug("Opening the file '{}'.", fileName);
+        try (InputStream fileStream = Files.newInputStream(filePath)) {
+            logger.debug("Parsing the yaml file '{}'.", fileName);
+            properties = new Yaml().load(fileStream);
+            logger.debug("The configuration yaml file '{}' was parsed successfully.", fileName);
+        }
+
+        if (properties == null || properties.isEmpty()) {
+            throw new IOException(String.format(
+                    "The configuration yaml file %s is empty or was not loaded successfully. ", fileName
+            ));
+        }
+
+        try {
+            PropertyConfig dataProviderProperties = createPropertyConfig(properties, "dataProvider");
+            DataProviderConfig dataProviderConfig = DataProviderConfig.create(dataProviderProperties);
+
+            PropertyConfig messageBrokerProperties = createPropertyConfig(properties, "messageBroker");
+            MessageBrokerConfig messageBrokerConfig = MessageBrokerConfig.create(messageBrokerProperties);
+
+            PropertyConfig monitoredObjectsProperties = createPropertyConfig(properties, "monitoredObjects");
+            MonitoredObjectsConfig monitoredObjectsConfig = MonitoredObjectsConfig.create(monitoredObjectsProperties);
+
+            return new Configuration(dataProviderConfig, messageBrokerConfig, monitoredObjectsConfig);
+        } catch (IOException e) {
+            throw new IOException(String.format(
+                    "Configuration file '%s' contains an error at '%s' attribute.", fileName, e.getMessage()
+            ));
+        }
+    }
+
+    private static PropertyConfig createPropertyConfig(Map<Object, Object> properties, String propertyName) throws IOException {
+        Object generalConfig = properties.get(propertyName);
+        if (!(generalConfig instanceof Map)) { throw new IOException(propertyName); }
+
+        Map<?, ?> generalConfigMap = (Map<?, ?>) generalConfig;
+        PropertyConfig propertyConfig = new PropertyConfig(propertyName);
+
+        for (Map.Entry<?, ?> entry : generalConfigMap.entrySet()) {
+            Object keyName = entry.getKey();
+            propertyConfig.setProperty(keyName.toString().toLowerCase(), entry.getValue());
+        }
+        return propertyConfig;
+    }
+}

+ 25 - 0
src/main/java/cz/senslog/watchdog/config/DataProviderConfig.java

@@ -0,0 +1,25 @@
+package cz.senslog.watchdog.config;
+
+public class DataProviderConfig {
+
+    private final DataProviderType type;
+
+    public static DataProviderConfig create(PropertyConfig config) {
+        DataProviderType type = DataProviderType.of(config.getStringProperty("type"));
+        PropertyConfig propertyConfig = config.getPropertyConfig("config");
+
+        switch (type) {
+            case DATABASE: return DatabaseConfig.create(propertyConfig);
+        }
+
+        return new DataProviderConfig(type);
+    }
+
+    protected DataProviderConfig(DataProviderType type) {
+        this.type = type;
+    }
+
+    public DataProviderType getType() {
+        return type;
+    }
+}

+ 10 - 0
src/main/java/cz/senslog/watchdog/config/DataProviderType.java

@@ -0,0 +1,10 @@
+package cz.senslog.watchdog.config;
+
+public enum DataProviderType {
+    DATABASE, WEB_SERVICE
+
+    ;
+    public static DataProviderType of(String value) {
+        return valueOf(value.toUpperCase());
+    }
+}

+ 49 - 0
src/main/java/cz/senslog/watchdog/config/DatabaseConfig.java

@@ -0,0 +1,49 @@
+package cz.senslog.watchdog.config;
+
+public class DatabaseConfig extends DataProviderConfig {
+
+    private final String connectionUrl;
+    private final String username;
+    private final String password;
+    private final Integer connectionPoolSize;
+    private final String groupName;
+
+    public static DatabaseConfig create(PropertyConfig config) {
+        return new DatabaseConfig(
+                config.getStringProperty("url"),
+                config.getStringProperty("username"),
+                config.getStringProperty("password"),
+                config.getIntegerProperty("connectionPoolSize"),
+                config.getStringProperty("groupName")
+        );
+    }
+
+    public DatabaseConfig(String connectionUrl, String username, String password, Integer poolSize, String groupName) {
+        super(DataProviderType.DATABASE);
+        this.connectionUrl = connectionUrl;
+        this.username = username;
+        this.password = password;
+        this.connectionPoolSize = poolSize;
+        this.groupName = groupName;
+    }
+
+    public String getConnectionUrl() {
+        return connectionUrl;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public Integer getConnectionPoolSize() {
+        return connectionPoolSize;
+    }
+
+    public String getGroupName() {
+        return groupName;
+    }
+}

+ 63 - 0
src/main/java/cz/senslog/watchdog/config/EmailMessageBrokerConfig.java

@@ -0,0 +1,63 @@
+package cz.senslog.watchdog.config;
+
+public class EmailMessageBrokerConfig extends MessageBrokerConfig {
+
+    private final String host;
+    private final int port;
+    private final String username;
+    private final String pass;
+    private final String sender;
+    private final String recipient;
+    private final String subject;
+
+    public static EmailMessageBrokerConfig create(PropertyConfig config) {
+        return new EmailMessageBrokerConfig(
+                config.getStringProperty("smtpHost"),
+                config.getIntegerProperty("smtpPort"),
+                config.getStringProperty("authUsername"),
+                config.getStringProperty("authPassword"),
+                config.getStringProperty("senderEmail"),
+                config.getStringProperty("recipientEmail"),
+                config.getStringProperty("subject")
+        );
+    }
+
+    public EmailMessageBrokerConfig(String host, int port, String username, String pass, String sender, String recipient, String subject) {
+        super(MessageBrokerType.EMAIL);
+        this.host = host;
+        this.port = port;
+        this.username = username;
+        this.pass = pass;
+        this.sender = sender;
+        this.recipient = recipient;
+        this.subject = subject;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getPass() {
+        return pass;
+    }
+
+    public String getSender() {
+        return sender;
+    }
+
+    public String getRecipient() {
+        return recipient;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+}

+ 26 - 0
src/main/java/cz/senslog/watchdog/config/MessageBrokerConfig.java

@@ -0,0 +1,26 @@
+package cz.senslog.watchdog.config;
+
+public class MessageBrokerConfig {
+
+    private final MessageBrokerType type;
+
+    public static MessageBrokerConfig create(PropertyConfig config) {
+        MessageBrokerType type = MessageBrokerType.of(config.getStringProperty("type"));
+        PropertyConfig propertyConfig = config.getPropertyConfig("config");
+
+        switch (type) {
+            case EMAIL: return EmailMessageBrokerConfig.create(propertyConfig);
+        }
+
+        return new MessageBrokerConfig(type);
+    }
+
+    protected MessageBrokerConfig(MessageBrokerType type) {
+        this.type = type;
+    }
+
+
+    public MessageBrokerType getType() {
+        return type;
+    }
+}

+ 11 - 0
src/main/java/cz/senslog/watchdog/config/MessageBrokerType.java

@@ -0,0 +1,11 @@
+package cz.senslog.watchdog.config;
+
+public enum MessageBrokerType {
+
+    EMAIL, WHATSAPP, TELEGRAM, CONSOLE
+
+    ;
+    public static MessageBrokerType of(String value) {
+        return valueOf(value.toUpperCase());
+    }
+}

+ 165 - 0
src/main/java/cz/senslog/watchdog/config/MonitoredObjectsConfig.java

@@ -0,0 +1,165 @@
+package cz.senslog.watchdog.config;
+
+import cz.senslog.watchdog.provider.Record;
+import cz.senslog.watchdog.util.Tuple;
+
+import java.util.*;
+
+public class MonitoredObjectsConfig {
+
+    private static class Unit {
+        final String id;
+        final long interval;
+        final boolean includeAll;
+        final Map<String, Sensor> sensors;
+
+        static Unit empty() {
+            return new Unit("__empty", Long.MAX_VALUE);
+        }
+
+        Unit(String id, long interval) {
+            if (interval == -1) {
+                throw new IllegalArgumentException(String.format(
+                    "The unit '%s' does not contain 'interval' argument.", id
+                ));
+            }
+            this.id = id;
+            this.interval = interval;
+            this.includeAll = true;
+            this.sensors = Collections.emptyMap();
+        }
+
+        Unit(String id, Map<String, Sensor> sensors) {
+            this.id = id;
+            this.interval = -1;
+            this.includeAll = false;
+            this.sensors = sensors;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Unit unit = (Unit) o;
+            return id.equals(unit.id);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id);
+        }
+    }
+
+    private static class Sensor {
+        final String id;
+        final long interval;
+
+        static Sensor empty() {
+            return new Sensor("__empty", Long.MAX_VALUE);
+        }
+
+        Sensor(String id, long interval) {
+            if (interval == -1) {
+                throw new IllegalArgumentException(String.format(
+                        "The sensor '%s' does not contain a correct 'interval' argument.", id
+                ));
+            }
+            this.id = id;
+            this.interval = interval;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Sensor sensor = (Sensor) o;
+            return id.equals(sensor.id);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id);
+        }
+    }
+
+    private final Map<String, Unit> units;
+    private final long minInterval, maxInterval;
+
+    public static MonitoredObjectsConfig create(PropertyConfig config) {
+        Set<String> unitIds = config.getAttributes();
+        Map<String, Unit> units = new HashMap<>(unitIds.size());
+        long minInterval = Long.MAX_VALUE, maxInterval = Long.MIN_VALUE;
+        for (String unitId : unitIds) {
+            PropertyConfig unitConfig = config.getPropertyConfig(unitId);
+            long unitInterval = -1;
+            if (unitConfig.containsProperty("interval")) {
+                unitInterval = unitConfig.getIntegerProperty("interval");
+            }
+            if (unitConfig.containsProperty("sensors")) {
+                Map<String, Sensor> sensors = new HashMap<>();
+                Object unitSensorsObj = unitConfig.getProperty("sensors");
+                if (unitSensorsObj instanceof List) {
+                    List<?> unitSensors = (List<?>)unitSensorsObj;
+                    for (Object sensorIdObj : unitSensors) {
+                        if (sensorIdObj instanceof Integer) {
+                            long sensorId = (Integer)sensorIdObj;
+                            String sensorIdStr = String.valueOf(sensorId);
+                            sensors.put(sensorIdStr, new Sensor(sensorIdStr, unitInterval));
+                        }
+                    }
+                } else if (unitSensorsObj instanceof Map) {
+                    PropertyConfig sensorsConfig = unitConfig.getPropertyConfig("sensors");
+                    Set<String> unitSensorIds = sensorsConfig.getAttributes();
+                    for (String sensorId : unitSensorIds) {
+                        Object sensorIntervalObj = sensorsConfig.getProperty(sensorId);
+                        long sensorInterval = sensorIntervalObj != null ? (Integer) sensorIntervalObj : unitInterval;
+                        sensors.put(sensorId, new Sensor(sensorId, sensorInterval));
+
+                        minInterval = Math.min(sensorInterval, minInterval);
+                        maxInterval = Math.max(sensorInterval, maxInterval);
+                    }
+                }
+                units.put(unitId, new Unit(unitId, sensors));
+            } else {
+                units.put(unitId, new Unit(unitId, unitInterval));
+                minInterval = Math.min(unitInterval, minInterval);
+                maxInterval = Math.max(unitInterval, maxInterval);
+            }
+        }
+        return new MonitoredObjectsConfig(units, minInterval, maxInterval);
+    }
+
+    public MonitoredObjectsConfig(Map<String, Unit> units, long minInterval, long maxInterval) {
+        this.units = units;
+        this.minInterval = minInterval;
+        this.maxInterval = maxInterval;
+    }
+
+    public Optional<Long> getInterval(Record record) {
+        Tuple<String, String> source = record.getSource();
+        String unitId = source.getItem1();
+        String sensorId = source.getItem2();
+        Unit unit = units.get(unitId);
+        if (unit == null) {
+            return Optional.empty();
+        }
+        if (unit.includeAll) {
+            return Optional.of(unit.interval);
+        }
+
+        Sensor sensor = unit.sensors.get(sensorId);
+        if (sensor == null) {
+            return Optional.empty();
+        }
+
+        return Optional.of(sensor.interval);
+    }
+
+    public long getMinInterval() {
+        return minInterval;
+    }
+
+    public long getMaxInterval() {
+        return maxInterval;
+    }
+}

+ 189 - 0
src/main/java/cz/senslog/watchdog/config/PropertyConfig.java

@@ -0,0 +1,189 @@
+// Copyright (c) 2020 UWB & LESP.
+// The UWB & LESP license this file to you under the MIT license.
+
+package cz.senslog.watchdog.config;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.*;
+
+import static java.lang.String.format;
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+import static java.util.Optional.ofNullable;
+
+/**
+ *
+ * @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.
+     */
+    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 IllegalArgumentException(format(
+                "Property '%s' does not exist.", getNewPropertyId(name))
+        );
+    }
+
+    /**
+     * Checks if property key is presents in properties.
+     * @param name - name of property
+     * @return boolean
+     */
+    public boolean containsProperty(String name) {
+        return properties.containsKey(name);
+    }
+
+    /**
+     * Returns optional value. It could be anything.
+     * @param name - name of property.
+     * @return optional object
+     */
+    public Optional<Object> getOptionalProperty(String name) {
+        return ofNullable(properties.get(name));
+    }
+
+    /**
+     * Returns property as a String.
+     * @param name - name of property.
+     * @return string value.
+     */
+    public String getStringProperty(String name) {
+        Object value = getProperty(name);
+        if (value instanceof String) {
+            return (String)value;
+        }
+        throw new ClassCastException(format(
+                "Value '%s' can not be cast to String", value
+        ));
+    }
+
+    /**
+     * Returns property as an Integer.
+     * @param name - name of property.
+     * @return integer value.
+     */
+    public Integer getIntegerProperty(String name) {
+        Object value = getProperty(name);
+        if (value instanceof Integer) {
+            return (Integer)value;
+        }
+        throw new ClassCastException(format(
+                "Value '%s' can not be cast to Integer", value
+        ));
+    }
+
+    /**
+     * Returns property as a LocalDateTime.
+     * @param name - name of property.
+     * @return localDateTime value.
+     */
+    public LocalDateTime getLocalDateTimeProperty(String name) {
+        Object object = getProperty(name);
+
+        if (object instanceof LocalDateTime) {
+            return (LocalDateTime) object;
+        } else if (object instanceof Date) {
+            Date date = (Date) object;
+            return date.toInstant().atZone(ZoneOffset.systemDefault()).toLocalDateTime();
+        } else if (object instanceof String) {
+            return LocalDateTime.parse((String)object, DateTimeFormatter.ISO_DATE_TIME);
+        } else {
+            throw new ClassCastException(format(
+                    "Property '%s' can not be cast to %s", getNewPropertyId(name), LocalDateTime.class)
+            );
+        }
+    }
+
+    public ZonedDateTime getZonedDateTimeProperty(String name) {
+        Object object = getProperty(name);
+
+        if (object instanceof ZonedDateTime) {
+            return (ZonedDateTime)object;
+        } else if (object instanceof String) {
+            final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
+                    .append(ISO_LOCAL_DATE_TIME)
+                    .optionalStart()
+                    .appendLiteral('[')
+                    .parseCaseSensitive()
+                    .appendZoneRegionId()
+                    .appendLiteral(']')
+                    .toFormatter();
+            return ZonedDateTime.parse((String)object, formatter);
+        } else {
+            throw new ClassCastException(format(
+                    "Property '%s' can not be cast to %s", getNewPropertyId(name), ZonedDateTime.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<?, ?> properties = (Map<?, ?>) property;
+            for (Map.Entry<?, ?> propertyEntry : properties.entrySet()) {
+                Object propertyName = propertyEntry.getKey();
+                config.setProperty(propertyName.toString(), propertyEntry.getValue());
+            }
+        }
+
+        return config;
+    }
+
+    public Set<String> getAttributes() {
+        return properties.keySet();
+    }
+
+    private String getNewPropertyId(String name) {
+        return id + PATH_DELIMITER + name;
+    }
+
+    public String getId() {
+        return id;
+    }
+}

+ 6 - 0
src/main/java/cz/senslog/watchdog/messagebroker/BlockingMessageBroker.java

@@ -0,0 +1,6 @@
+package cz.senslog.watchdog.messagebroker;
+
+public abstract class BlockingMessageBroker implements MessageBroker {
+
+
+}

+ 21 - 0
src/main/java/cz/senslog/watchdog/messagebroker/ConsoleMessageBroker.java

@@ -0,0 +1,21 @@
+package cz.senslog.watchdog.messagebroker;
+
+import cz.senslog.watchdog.config.MessageBrokerType;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class ConsoleMessageBroker implements MessageBroker {
+
+    private static final Logger logger = LogManager.getLogger(ConsoleMessageBroker.class);
+
+    @Override
+    public void send(String text, MessageBrokerHandler status) {
+        logger.warn("Sending a message to the console: '{}'.", text);
+        status.handle(new MessageStatus(text));
+    }
+
+    @Override
+    public MessageBrokerType getType() {
+        return MessageBrokerType.CONSOLE;
+    }
+}

+ 95 - 0
src/main/java/cz/senslog/watchdog/messagebroker/EmailMessageBroker.java

@@ -0,0 +1,95 @@
+package cz.senslog.watchdog.messagebroker;
+
+import cz.senslog.watchdog.config.EmailMessageBrokerConfig;
+import cz.senslog.watchdog.config.MessageBrokerType;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.mail.*;
+import javax.mail.internet.*;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static javax.mail.Message.RecipientType.TO;
+
+public class EmailMessageBroker implements MessageBroker {
+
+    private static final Logger logger = LogManager.getLogger(EmailMessageBroker.class);
+
+    private final EmailMessageBrokerConfig config;
+    private final Session session;
+
+    private final Map<String, LocalDateTime> sendMessages;
+
+    public EmailMessageBroker(EmailMessageBrokerConfig config) {
+        this.config = config;
+        this.session = openSession(config);
+        this.sendMessages = new HashMap<>();
+    }
+
+    private Session openSession(EmailMessageBrokerConfig config) {
+        Properties props = new Properties();
+        props.put("mail.smtp.host", config.getHost());
+        props.put("mail.smtp.port", config.getPort());
+        props.put("mail.smtp.auth", "true");
+        props.put("mail.smtp.starttls.enable", "true");
+        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
+
+        Authenticator auth = new Authenticator() {
+            protected PasswordAuthentication getPasswordAuthentication() {
+                return new PasswordAuthentication(config.getUsername(), config.getPass());
+            }
+        };
+        return Session.getInstance(props, auth);
+    }
+
+    private Message createMessage(String text) throws MessagingException {
+        Message message = new MimeMessage(session);
+        message.setFrom(new InternetAddress(config.getSender()));
+        message.setRecipients(TO, InternetAddress.parse(config.getRecipient()));
+        message.setSubject(config.getSubject());
+
+        MimeBodyPart mimeBodyPart = new MimeBodyPart();
+        mimeBodyPart.setContent(text, "text/html");
+
+        Multipart multipart = new MimeMultipart();
+        multipart.addBodyPart(mimeBodyPart);
+        message.setContent(multipart);
+
+        return message;
+    }
+
+    @Override
+    public void send(String text, MessageBrokerHandler status) {
+        if (sendMessages.containsKey(text)) {
+            logger.info("The message '{}' was already send at {}.", text, sendMessages.get(text));
+            status.handle(new MessageStatus(text)); return;
+        }
+
+        try {
+            Message message = createMessage(text);
+            logger.info("Sending a message via email.");
+            Transport.send(message);
+            logger.info("The message was send successfully.");
+            sendMessages.put(text, LocalDateTime.now());
+            status.handle(new MessageStatus(text));
+        } catch (MessagingException e) {
+            logger.catching(e);
+            status.handle(new MessageStatus(text, e.getMessage()));
+        }
+
+        LocalDateTime deadline = LocalDateTime.now().minusDays(1);
+        for (Map.Entry<String, LocalDateTime> entry : sendMessages.entrySet()) {
+            if (entry.getValue().isBefore(deadline)) {
+                sendMessages.remove(entry.getKey());
+            }
+        }
+    }
+
+    @Override
+    public MessageBrokerType getType() {
+        return MessageBrokerType.EMAIL;
+    }
+}

+ 22 - 0
src/main/java/cz/senslog/watchdog/messagebroker/MessageBroker.java

@@ -0,0 +1,22 @@
+package cz.senslog.watchdog.messagebroker;
+
+import cz.senslog.watchdog.config.EmailMessageBrokerConfig;
+import cz.senslog.watchdog.config.MessageBrokerConfig;
+import cz.senslog.watchdog.config.MessageBrokerType;
+
+public interface MessageBroker {
+
+    static MessageBroker create(MessageBrokerConfig config) {
+        switch (config.getType()) {
+            case EMAIL: return new EmailMessageBroker((EmailMessageBrokerConfig) config);
+            case CONSOLE: return new ConsoleMessageBroker();
+            default: throw new RuntimeException(String.format(
+                    "The message broker '%s' is not implemented yet.", config.getType())
+            );
+        }
+    }
+
+    void send(String message, MessageBrokerHandler status);
+
+    MessageBrokerType getType();
+}

+ 6 - 0
src/main/java/cz/senslog/watchdog/messagebroker/MessageBrokerHandler.java

@@ -0,0 +1,6 @@
+package cz.senslog.watchdog.messagebroker;
+
+@FunctionalInterface
+public interface MessageBrokerHandler {
+    void handle(MessageStatus status);
+}

+ 33 - 0
src/main/java/cz/senslog/watchdog/messagebroker/MessageStatus.java

@@ -0,0 +1,33 @@
+package cz.senslog.watchdog.messagebroker;
+
+public class MessageStatus {
+
+    private final String message;
+    private final String error;
+
+    public MessageStatus(String message) {
+        this.message = message;
+        this.error = null;
+    }
+
+    public MessageStatus(String message, String error) {
+        this.message = message;
+        this.error = error;
+    }
+
+    public boolean isSuccess() {
+        return error == null;
+    }
+
+    public boolean isError() {
+        return !isSuccess();
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String getError() {
+        return error;
+    }
+}

+ 21 - 0
src/main/java/cz/senslog/watchdog/provider/DataProvider.java

@@ -0,0 +1,21 @@
+package cz.senslog.watchdog.provider;
+
+import cz.senslog.watchdog.config.DataProviderConfig;
+import cz.senslog.watchdog.config.DatabaseConfig;
+import cz.senslog.watchdog.provider.database.DatabaseDataProvider;
+import cz.senslog.watchdog.provider.ws.WebServiceDataProvider;
+
+import java.util.List;
+
+public interface DataProvider {
+
+    static DataProvider create(DataProviderConfig config) {
+        switch (config.getType()) {
+            case DATABASE: return new DatabaseDataProvider((DatabaseConfig) config);
+            case WEB_SERVICE: return new WebServiceDataProvider();
+            default: return null;
+        }
+    }
+
+    List<Record> getLastRecords();
+}

+ 13 - 0
src/main/java/cz/senslog/watchdog/provider/Record.java

@@ -0,0 +1,13 @@
+package cz.senslog.watchdog.provider;
+
+
+import cz.senslog.watchdog.util.Tuple;
+
+import java.time.Instant;
+
+public interface Record {
+
+    Tuple<String, String> getSource();
+
+    boolean isValid(long interval, Instant now);
+}

+ 38 - 0
src/main/java/cz/senslog/watchdog/provider/database/Connection.java

@@ -0,0 +1,38 @@
+package cz.senslog.watchdog.provider.database;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import cz.senslog.watchdog.config.DatabaseConfig;
+import org.jdbi.v3.core.Jdbi;
+import org.jdbi.v3.jodatime2.JodaTimePlugin;
+import org.jdbi.v3.postgres.PostgresPlugin;
+import org.postgresql.ds.PGSimpleDataSource;
+
+public class Connection<T> {
+
+    private final T connection;
+
+    public static Connection<Jdbi> create(DatabaseConfig config) {
+        PGSimpleDataSource ds = new PGSimpleDataSource();
+        ds.setUrl(config.getConnectionUrl());
+        ds.setPassword(config.getPassword());
+        ds.setUser(config.getUsername());
+        ds.setLoadBalanceHosts(true);
+        HikariConfig hc = new HikariConfig();
+        hc.setDataSource(ds);
+        hc.setMaximumPoolSize(config.getConnectionPoolSize());
+        Jdbi jdbiConnection = Jdbi
+                .create(new HikariDataSource(hc))
+                .installPlugin(new PostgresPlugin())
+                .installPlugin(new JodaTimePlugin());
+        return new Connection<>(jdbiConnection);
+    }
+
+    private Connection(T connection) {
+        this.connection = connection;
+    }
+
+    public T get() {
+        return connection;
+    }
+}

+ 27 - 0
src/main/java/cz/senslog/watchdog/provider/database/DatabaseDataProvider.java

@@ -0,0 +1,27 @@
+package cz.senslog.watchdog.provider.database;
+
+import cz.senslog.watchdog.config.DatabaseConfig;
+import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.provider.Record;
+import org.jdbi.v3.core.Jdbi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DatabaseDataProvider implements DataProvider {
+
+    private final SensLogRepository repository;
+    private final String groupName;
+
+    public DatabaseDataProvider(DatabaseConfig config) {
+        Connection<Jdbi> connection = Connection.create(config);
+        this.repository = new SensLogRepository(connection);
+        this.groupName = config.getGroupName();
+    }
+
+    @Override
+    public List<Record> getLastRecords() {
+        List<ObservationInfo> lastObservations = repository.getLastObservations(groupName);
+        return new ArrayList<>(lastObservations);
+    }
+}

+ 55 - 0
src/main/java/cz/senslog/watchdog/provider/database/ObservationInfo.java

@@ -0,0 +1,55 @@
+package cz.senslog.watchdog.provider.database;
+
+import cz.senslog.watchdog.provider.Record;
+import cz.senslog.watchdog.util.Tuple;
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.util.Objects;
+
+public class ObservationInfo implements Record {
+
+    private final long unitId;
+    private final long sensorId;
+    private final OffsetDateTime timestamp;
+
+    public ObservationInfo(long unitId, long sensorId, OffsetDateTime timestamp) {
+        this.unitId = unitId;
+        this.sensorId = sensorId;
+        this.timestamp = timestamp;
+    }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "unitId=" + unitId +
+                ", sensorId=" + sensorId +
+                ", timestamp=" + timestamp +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ObservationInfo info = (ObservationInfo) o;
+        return unitId == info.unitId &&
+                sensorId == info.sensorId &&
+                Objects.equals(timestamp, info.timestamp);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(unitId, sensorId, timestamp);
+    }
+
+    @Override
+    public Tuple<String, String> getSource() {
+        return Tuple.of(String.valueOf(unitId), String.valueOf(sensorId));
+    }
+
+    @Override
+    public boolean isValid(long interval, Instant now) {
+        return timestamp.plusSeconds(interval).toInstant().isAfter(now);
+    }
+}

+ 47 - 0
src/main/java/cz/senslog/watchdog/provider/database/SensLogRepository.java

@@ -0,0 +1,47 @@
+package cz.senslog.watchdog.provider.database;
+
+import org.jdbi.v3.core.Jdbi;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.List;
+
+public class SensLogRepository {
+
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
+            .append(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+            .optionalStart().appendOffset("+HH", "+00").optionalEnd()
+            .toFormatter();
+
+    private final Jdbi jdbi;
+
+    public SensLogRepository(Connection<Jdbi> connection) {
+        this.jdbi = connection.get();
+    }
+
+    public List<ObservationInfo> getLastObservations(String groupName) {
+        return jdbi.withHandle(h -> h.createQuery(
+                "SELECT " +
+                        "time_stamp AS timestamp, " +
+                        "observed_value AS observed_value, " +
+                        "o.sensor_id AS sensor_id, " +
+                        "o.unit_id AS unit_id " +
+                        "FROM groups g, units_to_groups utg, units_to_sensors uts " +
+                        "LEFT JOIN observations o ON uts.last_obs = o.time_stamp " +
+                        "WHERE g.group_name = :groupName " +
+                        "  AND g.id = utg.group_id " +
+                        "  AND utg.unit_id = uts.unit_id " +
+                        "  AND uts.unit_id = o.unit_id " +
+                        "  AND uts.sensor_id = o.sensor_id " +
+                        "ORDER BY uts.unit_id, uts.sensor_id;"
+                )
+                        .bind("groupName", groupName)
+                        .map((rs, ctx) -> new ObservationInfo(
+                                rs.getLong("unit_id"),
+                                rs.getLong("sensor_id"),
+                                OffsetDateTime.parse(rs.getString("timestamp"), DATE_TIME_FORMATTER)
+                        )).list()
+        );
+    }
+}

+ 15 - 0
src/main/java/cz/senslog/watchdog/provider/ws/WebServiceDataProvider.java

@@ -0,0 +1,15 @@
+package cz.senslog.watchdog.provider.ws;
+
+import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.provider.Record;
+
+import java.util.Collections;
+import java.util.List;
+
+public class WebServiceDataProvider implements DataProvider {
+
+    @Override
+    public List<Record> getLastRecords() {
+        return Collections.emptyList();
+    }
+}

+ 22 - 0
src/main/java/cz/senslog/watchdog/util/StringUtils.java

@@ -0,0 +1,22 @@
+package cz.senslog.watchdog.util;
+
+public final class StringUtils {
+    public StringUtils() {
+    }
+
+    public static boolean isEmpty(String string) {
+        return string == null || string.isEmpty();
+    }
+
+    public static boolean isBlank(String string) {
+        return string == null || string.replaceAll("\\s+", "").isEmpty();
+    }
+
+    public static boolean isNotEmpty(String string) {
+        return !isEmpty(string);
+    }
+
+    public static boolean isNotBlank(String string) {
+        return !isBlank(string);
+    }
+}

+ 40 - 0
src/main/java/cz/senslog/watchdog/util/Tuple.java

@@ -0,0 +1,40 @@
+package cz.senslog.watchdog.util;
+
+import java.util.Objects;
+
+public class Tuple<A, B> {
+    private final A item1;
+    private final B item2;
+
+    public static <A, B> Tuple<A, B> of(A item1, B item2) {
+        return new Tuple<>(item1, item2);
+    }
+
+    private Tuple(A item1, B item2) {
+        this.item1 = item1;
+        this.item2 = item2;
+    }
+
+    public A getItem1() {
+        return this.item1;
+    }
+
+    public B getItem2() {
+        return this.item2;
+    }
+
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        } else if (o != null && this.getClass() == o.getClass()) {
+            Tuple<?, ?> tuple = (Tuple<?, ?>)o;
+            return this.item1.equals(tuple.item1) && this.item2.equals(tuple.item2);
+        } else {
+            return false;
+        }
+    }
+
+    public int hashCode() {
+        return Objects.hash(this.item1, this.item2);
+    }
+}

+ 50 - 0
src/main/java/cz/senslog/watchdog/util/schedule/ScheduleTask.java

@@ -0,0 +1,50 @@
+package cz.senslog.watchdog.util.schedule;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+public final class ScheduleTask {
+
+    private static final int DELAY = 2;
+
+    private TaskDescription description;
+    private final Runnable task;
+    private final long period;
+
+    public ScheduleTask(String name, Runnable task, long period) {
+        this.description = new TaskDescription(name, Status.STOPPED);
+        this.task = task;
+        this.period = period;
+    }
+
+    public TaskDescription getDescription() {
+        return description;
+    }
+
+    public Runnable getTask() {
+        return task;
+    }
+
+    public long getPeriod() {
+        return period;
+    }
+
+    public void schedule(ScheduledExecutorService scheduledService, CountDownLatch latch) {
+        ScheduledFuture<?> future = scheduledService.scheduleAtFixedRate(getTask(), DELAY, getPeriod(), SECONDS);
+        description = new TaskDescription(description.getName(), Status.RUNNING);
+        new Thread(() -> {
+            try {
+                future.get();
+            } catch (Exception e) {
+                e.printStackTrace();
+            } finally {
+                future.cancel(true);
+                latch.countDown();
+                description = new TaskDescription(description.getName(), Status.STOPPED);
+            }
+        }, "thread-"+description.getName()).start();
+    }
+}

+ 24 - 0
src/main/java/cz/senslog/watchdog/util/schedule/Scheduler.java

@@ -0,0 +1,24 @@
+package cz.senslog.watchdog.util.schedule;
+
+import java.util.Set;
+
+public interface Scheduler {
+
+    static SchedulerBuilder createBuilder() {
+        return new SchedulerBuilderImpl();
+    }
+
+    void start();
+    void stop();
+
+    Status getStatus();
+    Set<TaskDescription> getTaskDescriptions();
+
+    interface SchedulerBuilder {
+
+        SchedulerBuilder addTask(String name, Runnable task, long period);
+        SchedulerBuilder addTask(Runnable task, long period);
+
+        Scheduler build();
+    }
+}

+ 30 - 0
src/main/java/cz/senslog/watchdog/util/schedule/SchedulerBuilderImpl.java

@@ -0,0 +1,30 @@
+package cz.senslog.watchdog.util.schedule;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class SchedulerBuilderImpl implements Scheduler.SchedulerBuilder {
+
+    private final Set<ScheduleTask> tasks;
+
+    public SchedulerBuilderImpl() {
+        this.tasks = new HashSet<>();
+    }
+
+    @Override
+    public Scheduler.SchedulerBuilder addTask(String name, Runnable task, long period) {
+        tasks.add(new ScheduleTask(name, task, period));
+        return this;
+    }
+
+    @Override
+    public Scheduler.SchedulerBuilder addTask(Runnable task, long period) {
+        tasks.add(new ScheduleTask(task.getClass().getSimpleName(), task, period));
+        return this;
+    }
+
+    @Override
+    public Scheduler build() {
+        return new SchedulerImpl(tasks);
+    }
+}

+ 60 - 0
src/main/java/cz/senslog/watchdog/util/schedule/SchedulerImpl.java

@@ -0,0 +1,60 @@
+package cz.senslog.watchdog.util.schedule;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class SchedulerImpl implements Scheduler {
+
+    private final Set<ScheduleTask> tasks;
+
+    private ScheduledExecutorService scheduler;
+    private CountDownLatch latch;
+
+    public SchedulerImpl(Set<ScheduleTask> tasks) {
+        this.tasks = tasks;
+    }
+
+    @Override
+    public void start() {
+
+        if (!tasks.isEmpty()) {
+            scheduler = Executors.newScheduledThreadPool(tasks.size());
+            latch = new CountDownLatch(tasks.size());
+            tasks.forEach(t -> t.schedule(scheduler, latch));
+
+            try {
+                latch.await();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        } else {
+            // TODO no tasks
+        }
+    }
+
+    @Override
+    public void stop() {
+        if (getStatus() == Status.RUNNING) {
+            scheduler.shutdown();
+            scheduler = null;
+        }
+    }
+
+    @Override
+    public Status getStatus() {
+        boolean active = scheduler != null && !scheduler.isShutdown();
+        return active ? Status.RUNNING : Status.STOPPED;
+    }
+
+    @Override
+    public Set<TaskDescription> getTaskDescriptions() {
+        Set<TaskDescription> descriptions = new HashSet<>(tasks.size());
+        for (ScheduleTask task : tasks) {
+            descriptions.add(task.getDescription());
+        }
+        return descriptions;
+    }
+}

+ 5 - 0
src/main/java/cz/senslog/watchdog/util/schedule/Status.java

@@ -0,0 +1,5 @@
+package cz.senslog.watchdog.util.schedule;
+
+public enum Status {
+    RUNNING, PREPARED, STOPPED,
+}

+ 29 - 0
src/main/java/cz/senslog/watchdog/util/schedule/TaskDescription.java

@@ -0,0 +1,29 @@
+package cz.senslog.watchdog.util.schedule;
+
+public class TaskDescription {
+
+    private final String name;
+
+    private final Status status;
+
+    public TaskDescription(String name, Status status) {
+        this.name = name;
+        this.status = status;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Status getStatus() {
+        return status;
+    }
+
+    @Override
+    public String toString() {
+        return "TaskDescription{" +
+                "name='" + name + '\'' +
+                ", status=" + status +
+                '}';
+    }
+}

+ 35 - 0
src/main/resources/log4j2.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="INFO">
+
+    <Properties>
+        <Property name="logPath">./</Property>
+    </Properties>
+
+    <Appenders>
+
+        <Console name="console" target="SYSTEM_OUT">
+            <PatternLayout pattern="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n" />
+        </Console>
+
+        <RollingFile name="filebeat"
+                     fileName="${sys:logPath}/app.log"
+                     filePattern="${sys:logPath}/app.%i.log.gz"
+        >
+            <PatternLayout alwaysWriteExceptions="false"
+                    pattern='{"app.date":"%d{ISO8601}","app.thread":"%t","app.level":"%level","app.logger":"%logger:%L", "app.exception":"%enc{%ex}{JSON}", "app.message":"%msg"}%n'
+            />
+            <Policies>
+<!--                <TimeBasedTriggeringPolicy interval="1"/> &lt;!&ndash; Number of days for a log file &ndash;&gt;-->
+                <SizeBasedTriggeringPolicy size="10MB" />
+            </Policies>
+        </RollingFile>
+
+    </Appenders>
+    <Loggers>
+        <Logger name="cz.senslog" level="info" />
+        <Root level="info">
+            <AppenderRef ref="console" />
+            <AppenderRef ref="filebeat" />
+        </Root>
+    </Loggers>
+</Configuration>