Browse Source

Init of v2.0

Lukas Cerny 1 year ago
parent
commit
a658b68c02
100 changed files with 2611 additions and 1674 deletions
  1. 41 0
      Dockerfile
  2. 0 20
      README.md
  3. 64 68
      build.gradle
  4. 0 60
      config/analyzer_init.sql
  5. 0 53
      config/config-test.yaml
  6. 0 53
      config/config.yaml
  7. 0 1
      config/db_update.sql
  8. BIN
      doc/Analytics.png
  9. 0 2
      doc/architecture.svg
  10. 0 1
      doc/architecture.xml
  11. BIN
      doc/data_loading_processing.png
  12. 0 1
      doc/data_loading_processing.xml
  13. BIN
      doc/data_model.png
  14. BIN
      doc/relation_model.png
  15. BIN
      doc/use_case_notify_instant.png
  16. BIN
      doc/use_case_notify_onchange.png
  17. BIN
      doc/use_case_tables.png
  18. 45 0
      docker-compose.yaml
  19. 16 0
      docker.dev.env
  20. 4 3
      gradle.properties
  21. 153 104
      gradlew
  22. 128 0
      init.sql
  23. 15 0
      local.dev.env
  24. 15 0
      sql/config.sql
  25. 67 0
      src/main/java/cz/senslog/analytics/MockData.java
  26. 97 0
      src/main/java/cz/senslog/analytics/app/Application.java
  27. 8 0
      src/main/java/cz/senslog/analytics/app/Main.java
  28. 152 0
      src/main/java/cz/senslog/analytics/app/PropertyConfig.java
  29. 94 0
      src/main/java/cz/senslog/analytics/app/VertxDeployer.java
  30. 2 2
      src/main/java/cz/senslog/analytics/domain/AttributeName.java
  31. 6 0
      src/main/java/cz/senslog/analytics/domain/CollectorType.java
  32. 145 0
      src/main/java/cz/senslog/analytics/domain/DoubleStatistics.java
  33. 16 0
      src/main/java/cz/senslog/analytics/domain/Group.java
  34. 6 0
      src/main/java/cz/senslog/analytics/domain/MessageBrokerConfig.java
  35. 11 0
      src/main/java/cz/senslog/analytics/domain/Observation.java
  36. 50 0
      src/main/java/cz/senslog/analytics/domain/Sensor.java
  37. 6 0
      src/main/java/cz/senslog/analytics/domain/SourceToMessageBroker.java
  38. 6 0
      src/main/java/cz/senslog/analytics/domain/StatisticRecord.java
  39. 9 0
      src/main/java/cz/senslog/analytics/domain/Threshold.java
  40. 4 0
      src/main/java/cz/senslog/analytics/domain/ThresholdRule.java
  41. 6 0
      src/main/java/cz/senslog/analytics/domain/ThresholdViolationNotification.java
  42. 10 0
      src/main/java/cz/senslog/analytics/domain/TimeSeriesDatasource.java
  43. 43 0
      src/main/java/cz/senslog/analytics/domain/ValidationResult.java
  44. 7 0
      src/main/java/cz/senslog/analytics/domain/ViolationReport.java
  45. 63 0
      src/main/java/cz/senslog/analytics/messaging/MessageBroker.java
  46. 21 0
      src/main/java/cz/senslog/analytics/messaging/SensLogAlertMessageBroker.java
  47. 62 0
      src/main/java/cz/senslog/analytics/module/DoubleStatisticsModule.java
  48. 26 0
      src/main/java/cz/senslog/analytics/module/MoldAnalysisModule.java
  49. 62 0
      src/main/java/cz/senslog/analytics/module/NotificationModule.java
  50. 108 0
      src/main/java/cz/senslog/analytics/module/ObservationReceiverModule.java
  51. 52 0
      src/main/java/cz/senslog/analytics/module/ScheduleDBLoaderModule.java
  52. 57 0
      src/main/java/cz/senslog/analytics/module/api/CollectedStatistics.java
  53. 136 0
      src/main/java/cz/senslog/analytics/module/api/CollectorModule.java
  54. 52 0
      src/main/java/cz/senslog/analytics/module/api/Module.java
  55. 5 0
      src/main/java/cz/senslog/analytics/module/api/ModuleDescriptor.java
  56. 36 0
      src/main/java/cz/senslog/analytics/module/api/SimpleModule.java
  57. 128 0
      src/main/java/cz/senslog/analytics/repository/AnalyticsConfigRepository.java
  58. 34 0
      src/main/java/cz/senslog/analytics/repository/AnalyticsDataRepository.java
  59. 22 0
      src/main/java/cz/senslog/analytics/repository/ConfigurationRepository.java
  60. 41 0
      src/main/java/cz/senslog/analytics/repository/MockConfigRepository.java
  61. 77 0
      src/main/java/cz/senslog/analytics/repository/SensLogDataRepository.java
  62. 8 22
      src/main/java/cz/senslog/analytics/utils/DateTrunc.java
  63. 12 0
      src/main/java/cz/senslog/analytics/utils/TimeUtils.java
  64. 39 0
      src/main/java/cz/senslog/analytics/utils/Tuple.java
  65. 22 0
      src/main/java/cz/senslog/analytics/utils/validator/DisableNotifyTrigger.java
  66. 48 0
      src/main/java/cz/senslog/analytics/utils/validator/InstantNotifyTrigger.java
  67. 34 0
      src/main/java/cz/senslog/analytics/utils/validator/NotifyTrigger.java
  68. 80 0
      src/main/java/cz/senslog/analytics/utils/validator/OnChangeNotifyTrigger.java
  69. 81 0
      src/main/java/cz/senslog/analytics/utils/validator/ThresholdChecker.java
  70. 79 0
      src/main/java/cz/senslog/analytics/utils/validator/Validator.java
  71. 0 24
      src/main/java/cz/senslog/analyzer/analysis/AggregationUnit.java
  72. 0 12
      src/main/java/cz/senslog/analyzer/analysis/Analyzer.java
  73. 0 15
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java
  74. 0 4
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerConfigInfo.java
  75. 0 88
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerInfo.java
  76. 0 60
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerModule.java
  77. 0 33
      src/main/java/cz/senslog/analyzer/analysis/Checker.java
  78. 0 16
      src/main/java/cz/senslog/analyzer/analysis/ManualAnalyticsConfig.java
  79. 0 38
      src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java
  80. 0 69
      src/main/java/cz/senslog/analyzer/analysis/TaskConfig.java
  81. 0 115
      src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java
  82. 0 48
      src/main/java/cz/senslog/analyzer/analysis/module/FilterHandler.java
  83. 0 79
      src/main/java/cz/senslog/analyzer/analysis/module/HandlersModule.java
  84. 0 53
      src/main/java/cz/senslog/analyzer/analysis/module/ThresholdHandler.java
  85. 0 127
      src/main/java/cz/senslog/analyzer/app/Application.java
  86. 0 10
      src/main/java/cz/senslog/analyzer/app/Main.java
  87. 0 78
      src/main/java/cz/senslog/analyzer/app/Parameters.java
  88. 0 13
      src/main/java/cz/senslog/analyzer/broker/AbstractBrokerConfig.java
  89. 0 39
      src/main/java/cz/senslog/analyzer/broker/AlertFormatter.java
  90. 0 5
      src/main/java/cz/senslog/analyzer/broker/BrokerType.java
  91. 0 28
      src/main/java/cz/senslog/analyzer/broker/database/DatabaseBroker.java
  92. 0 18
      src/main/java/cz/senslog/analyzer/broker/database/DatabaseBrokerConfig.java
  93. 0 38
      src/main/java/cz/senslog/analyzer/broker/email/EmailBroker.java
  94. 0 38
      src/main/java/cz/senslog/analyzer/broker/email/EmailMessageConfig.java
  95. 0 38
      src/main/java/cz/senslog/analyzer/broker/email/EmailServerConfig.java
  96. 0 61
      src/main/java/cz/senslog/analyzer/broker/email/EmailServerConnection.java
  97. 0 19
      src/main/java/cz/senslog/analyzer/broker/statistic/StatisticBroker.java
  98. 0 40
      src/main/java/cz/senslog/analyzer/core/AbstractEventBroker.java
  99. 0 63
      src/main/java/cz/senslog/analyzer/core/AbstractWatchableObjects.java
  100. 0 15
      src/main/java/cz/senslog/analyzer/core/EventBus.java

+ 41 - 0
Dockerfile

@@ -0,0 +1,41 @@
+FROM openjdk:17 AS builder
+
+COPY src /app/src
+#COPY keystore.jceks /app/keystore.jceks
+COPY gradle /app/gradle
+COPY build.gradle settings.gradle gradle.properties gradlew /app/
+WORKDIR /app
+RUN ./gradlew assemble
+
+FROM openjdk:17 AS test
+
+COPY --from=builder /app/build /app/build
+COPY --from=builder /app/gradle /app/gradle
+COPY --from=builder /app/build.gradle /app/
+COPY --from=builder /app/settings.gradle /app/
+COPY --from=builder /app/gradle.properties /app/
+COPY --from=builder /app/gradlew /app/
+
+WORKDIR /app
+
+RUN ./gradlew test
+
+FROM openjdk:17-jdk-slim-buster AS production
+
+COPY --from=builder /app/build/libs/ /app/
+COPY --from=builder /app/gradle.properties /app/
+#COPY --from=builder /app/keystore.jceks /app/
+
+WORKDIR /app
+
+CMD java -cp "analytics.jar" cz.senslog.analytics.app.Main
+
+FROM openjdk:17-jdk-slim-buster AS dev-debug
+
+COPY --from=builder /app/build/libs/ /app/
+COPY --from=builder /app/gradle.properties /app/
+#COPY --from=builder /app/keystore.jceks /app/
+
+WORKDIR /app
+
+CMD java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -cp "analytics.jar" cz.senslog.analytics.app.Main

+ 0 - 20
README.md

@@ -1,20 +0,0 @@
-Spusteni programu s konfiguraci
-
-java -jar analyzer-1.0.jar -cf config.yaml > /dev/null &
-
-
-Ukazka dotazu na API
-
-http://127.0.0.1:9090/statistics?
-group_id=<GROUP_ID>&
-from=<FROM>&
-to=<TO>&
-intervalGroup=<ENUM_GROUP>
-
-
-GROUP_ID -> long number
-FROM -> yyyy-MM-dd HH:mm:ss+HH
-TO -> yyyy-MM-dd HH:mm:ss+HH
-GROUP -> [DAY, MONTH, YEAR]
-
-

+ 64 - 68
build.gradle

@@ -1,68 +1,64 @@
-plugins {
-    id 'java'
-    id "io.dotinc.vertx-codegen-plugin" version "0.1.1"
-}
-
-group 'cz.senslog'
-version '1.2.2'
-//version '1.3-SNAPSHOT'
-
-sourceCompatibility = 17
-
-repositories {
-    mavenCentral()
-    mavenLocal()
-}
-
-codeGen {
-    vertxVersion = '4.4.3'
-    generatedDirs = "src/main/generated"
-    generationPath = "proxy"
-}
-
-test {
-    useJUnitPlatform()
-}
-
-jar {
-    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-
-    manifest {
-        attributes(
-                'Main-Class': 'cz.senslog.analyzer.app.Main'
-        )
-    }
-    from {
-        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
-    }
-}
-
-dependencies {
-    testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.9.3'
-    testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.4.0'
-    testImplementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.20.0'
-
-    implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.20.0'
-    implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.20.0'
-
-    implementation group: 'com.beust', name: 'jcommander', version: '1.82'
-    implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1'
-    implementation group: 'org.yaml', name: 'snakeyaml', version: '2.0'
-
-    implementation group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.2'
-    implementation group: 'com.sun.mail', name: 'jakarta.mail', version: '2.0.1'
-
-    implementation group: 'io.vertx', name: 'vertx-core', version: '4.4.3'
-    implementation group: 'io.vertx', name: 'vertx-web', version: '4.4.3'
-    annotationProcessor 'io.vertx:vertx-codegen:4.4.3:processor'
-    annotationProcessor 'io.vertx:vertx-service-proxy:4.4.3'
-
-    implementation group: 'org.jdbi', name: 'jdbi3-postgres', version: '3.39.1'
-    implementation group: 'org.jdbi', name: 'jdbi3-jodatime2', version: '3.39.1'
-    implementation group: 'org.postgresql', name: 'postgresql', version: '42.6.0'
-    implementation group: 'com.h2database', name: 'h2', version: '2.1.214'
-    implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'
-
-    implementation group: 'com.google.dagger', name: 'dagger', version: '2.46.1'
-    annotationProcessor 'com.google.dagger:dagger-compiler:2.46.1'
-}
+import java.time.Instant
+
+plugins {
+    id 'java'
+    id 'application'
+}
+
+group projectGroup
+version projectVersion
+
+jar.archiveFileName = "analytics.jar"
+
+application {
+    mainClass = 'cz.senslog.analytics.app.Main'
+}
+
+repositories {
+    mavenLocal()
+    mavenCentral()
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}
+
+build {
+    doLast {
+        ant.propertyfile(file: "gradle.properties") {
+            entry( key: "buildVersion", value: Instant.now().getEpochSecond())
+        }
+    }
+}
+
+jar {
+    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+    manifest {
+        attributes 'Implementation-Title': 'SensLog Analytics',
+                'Implementation-Version': version,
+                'Main-Class': 'cz.senslog.analytics.app.Main'
+    }
+    from {
+        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+    }
+}
+
+test {
+    useJUnitPlatform()
+}
+
+dependencies {
+    implementation 'org.apache.logging.log4j:log4j-api:2.22.0'
+    implementation 'org.apache.logging.log4j:log4j-core:2.22.0'
+
+    implementation 'io.vertx:vertx-core:4.5.7'
+    implementation 'io.vertx:vertx-web:4.5.7'
+    implementation 'io.vertx:vertx-pg-client:4.5.7'
+    implementation 'org.postgresql:postgresql:42.7.3'
+    implementation 'com.ongres.scram:client:2.1'
+
+    testImplementation platform('org.junit:junit-bom:5.9.2')
+    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
+}
+

+ 0 - 60
config/analyzer_init.sql

@@ -1,60 +0,0 @@
-
-\connect senslog1
-
-create schema if not exists statistics; 
-
-alter schema statistics OWNER TO senslog;
-
-/* table with calculated values */
-create table statistics.records (
-	id SERIAL PRIMARY KEY NOT NULL,
-	group_id bigint not null,
-	value_attribute text not null,      -- [MIN, MAX, SUM, COUNT]
-	record_value double precision not null,
-	time_interval integer not null,     -- interval in seconds
-	time_stamp timestamptz not null,
-    created timestamptz DEFAULT CURRENT_TIMESTAMP,
-    UNIQUE (group_id, value_attribute, time_stamp)
-);
-
-alter table statistics.records OWNER TO senslog;
-
-/* time interval to analyse sensor groups */
-create table statistics.groups_interval (
-    id SERIAL PRIMARY KEY NOT NULL,
-	time_interval integer not null,     -- interval in seconds
-    persistence boolean not null,
-    aggregation_type text not null      -- ['DOUBLE']
-);
-
-alter table statistics.groups_interval OWNER TO senslog;
-
-/* list of unit-sensor pair to run analyses */
-create table statistics.sensors (
-    id SERIAL PRIMARY KEY NOT NULL,
-    unit_id bigint not null,
-	sensor_id bigint not null,
-	CONSTRAINT fk_unit_sensor FOREIGN KEY (unit_id, sensor_id) REFERENCES public.units_to_sensors(unit_id, sensor_id)
-);
-
-alter table statistics.sensors OWNER TO senslog;
-
-/* thresholds to be checked */
-create table statistics.thresholds (
-    id SERIAL PRIMARY KEY NOT NULL,
-	group_id integer not null,
-	mode varchar(10) not null,      -- [le, lt, ge, gt, ne, eq]
-	property varchar(10) not null,  -- [MIN, MAX, AVG, VAL]
-	threshold_value double precision not null
-);
-
-alter table statistics.thresholds OWNER TO senslog;
-
-create table statistics.sensor_to_group (
-    id SERIAL PRIMARY KEY NOT NULL,
-    sensor_id integer REFERENCES statistics.sensors(id),
-    group_id integer REFERENCES statistics.groups_interval(id),
-    created timestamptz DEFAULT CURRENT_TIMESTAMP
-);
-
-alter table statistics.sensor_to_group OWNER TO senslog;

+ 0 - 53
config/config-test.yaml

@@ -1,53 +0,0 @@
-server:
-  port: 9090
-
-eventBus:
-  notifyBrokers: [  emailMessage,   dbAlert]
-
-
-emailServers:
-  lspEmail:
-    smtpHost: "mail.lesprojekt.cz"
-    smtpPort: 465
-    authUsername: "watchdog@senslog.org"
-    authPassword: "5jspdD"
-
-messageBrokers:
-  emailMessage:
-    type: EMAIL
-#    filter: [] // whiteList / blackList ...
-    config:
-      server: lspEmail
-      senderEmail: "analytics@senslog.org"
-      recipientEmail:
-        - "luccerny@ntis.zcu.cz"
-      subject: "[Analytics] Alert for the group $group_name"
-
-  dbAlert:
-    type: DATABASE
-#    filter: []
-    config:
-      msgPattern: "'$group_name' failed at $timestamp in $messages."
-
-storage:
-  permanent:
-    url: "jdbc:postgresql://localhost:5432/senslog1"
-    username: "senslog_app"
-    password: "SENSlog"
-    connectionPoolSize: 6
-
-  inMemory:
-    path: "./statistics"
-    persistence: true
-    parameters: ""
-
-scheduler:
-  initDate: "1970-02-01T10:02:00.00+00:00"
-  period: 10
-
-
-#manualAnalytics:
-#  osek:
-#    from: "1970-01-01T00:00:00.00+00:00"
-#    to: "2022-01-01T00:00:00.00+00:00"
-#    groupIds: [1]

+ 0 - 53
config/config.yaml

@@ -1,53 +0,0 @@
-server:
-  port: 9090
-
-eventBus:
-  notifyBrokers: [  emailMessage,   dbAlert]
-
-
-emailServers:
-  lspEmail:
-    smtpHost: "10.0.0.100" # "mail.lesprojekt.cz"
-    smtpPort: 465
-    authUsername: "watchdog@senslog.org"
-    authPassword: "5jspdD"
-
-messageBrokers:
-  emailMessage:
-    type: EMAIL
-#    filter: [] // whiteList / blackList ...
-    config:
-      server: lspEmail
-      senderEmail: "analytics@senslog.org"
-      recipientEmail:
-        - "luccerny@ntis.zcu.cz"
-      subject: "[Analytics] Alert for the group $group_name"
-
-  dbAlert:
-    type: DATABASE
-#    filter: []
-    config:
-      msgPattern: "'$group_name' failed at $timestamp in $messages."
-
-storage:
-  permanent:
-    url: "jdbc:postgresql://localhost:5432/senslog1"
-    username: "senslog_app"
-    password: "SENSlog"
-    connectionPoolSize: 6
-
-  inMemory:
-    path: "./statistics"
-    persistence: true
-    parameters: ""
-
-scheduler:
-  initDate: "1970-02-01T10:02:00.00+00:00"
-  period: 10
-
-
-manualAnalytics:
-  osek:
-    from: "1970-01-01T00:00:00.00+00:00"
-    to: "2022-01-01T00:00:00.00+00:00"
-    groupIds: [1649, 1650, 1651, 1652, 1653, 1654, 1655, 1656. 1657, 1658, 1659, 1660, 1661]

+ 0 - 1
config/db_update.sql

@@ -1 +0,0 @@
-ALTER TABLE statistics.groups_interval ADD COLUMN name varchar(256) not null default 'undefined';

BIN
doc/Analytics.png


File diff suppressed because it is too large
+ 0 - 2
doc/architecture.svg


File diff suppressed because it is too large
+ 0 - 1
doc/architecture.xml


BIN
doc/data_loading_processing.png


File diff suppressed because it is too large
+ 0 - 1
doc/data_loading_processing.xml


BIN
doc/data_model.png


BIN
doc/relation_model.png


BIN
doc/use_case_notify_instant.png


BIN
doc/use_case_notify_onchange.png


BIN
doc/use_case_tables.png


+ 45 - 0
docker-compose.yaml

@@ -0,0 +1,45 @@
+version: "3.9"
+
+services:
+  analytics-prod:
+    container_name: senslog_analytics
+    image: senslog/analytics
+    build:
+      target: production
+      context: .
+    ports:
+      - "8080:80"
+
+  analytics-dev:
+    container_name: senslog_analytics_dev
+    image: senslog/analytics-dev
+    build:
+      target: dev-debug
+      context: .
+    env_file:
+      - docker.dev.env
+    ports:
+      - "8080:8080"
+      - "5005:5005"
+    depends_on:
+      - analytics-db
+
+  analytics-test:
+    container_name: senslog_analytics_test
+    image: senslog/analytics-test
+    build:
+      target: test
+      context: .
+    depends_on:
+      - analytics-db
+
+  analytics-db:
+    image: postgis/postgis:15-3.3-alpine
+    container_name: analytics_db
+    environment:
+      - POSTGRES_USER=postgres
+      - POSTGRES_PASSWORD=postgres
+    ports:
+      - '5432:5432'
+    volumes:
+      - ./init.sql:/docker-entrypoint-initdb.d/create_tables.sql

+ 16 - 0
docker.dev.env

@@ -0,0 +1,16 @@
+# Servers
+SERVER_HTTP_PORT=8080
+
+# Database properties
+DATABASE_HOST=172.17.0.1
+DATABASE_PORT=5432
+DATABASE_NAME=analytics
+DATABASE_USER=analytics_app
+DATABASE_PASSWORD=analytics
+DATABASE_POOL_SIZE=5
+
+# Auth
+AUTH_DISABLED=true
+AUTH_KEYSTORE_PATH=/app/keystore.jceks
+AUTH_KEYSTORE_TYPE=PKCS12
+AUTH_KEYSTORE_PASSWORD=SENSlog

+ 4 - 3
gradle.properties

@@ -1,3 +1,4 @@
-#org.gradle.jvmargs=-Xmx10248m -XX:MaxPermSize=256m
-org.gradle.workers.max=4
-org.gradle.parallel=true
+#Sat, 01 Apr 2023 19:58:59 +0200
+projectGroup=cz.senslog
+projectName=analytics
+projectVersion=2.0.0

+ 153 - 104
gradlew

@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
 
 #
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
 #
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 
 APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
     echo
     echo "$*"
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
 cygwin=false
 msys=false
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
     fi
 else
-    JAVACMD="java"
+    JAVACMD=java
     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
 fi
 
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
 
 # For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
     # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        i=`expr $i + 1`
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    case $i in
-        0) set -- ;;
-        1) set -- "$args0" ;;
-        2) set -- "$args0" "$args1" ;;
-        3) set -- "$args0" "$args1" "$args2" ;;
-        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
 fi
 
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
 
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
 
 exec "$JAVACMD" "$@"

+ 128 - 0
init.sql

@@ -0,0 +1,128 @@
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+CREATE ROLE senslog NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
+CREATE ROLE analytics_app NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN PASSWORD 'analytics';
+GRANT senslog TO analytics_app;
+
+CREATE DATABASE analytics WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE_PROVIDER = 'libc' LOCALE = 'en_US.UTF-8';
+
+ALTER DATABASE analytics OWNER TO senslog;
+
+\connect analytics
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+SET default_tablespace = '';
+
+CREATE SCHEMA analytics;
+ALTER SCHEMA analytics OWNER TO senslog;
+ALTER SCHEMA public OWNER TO senslog;
+
+-- CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public;
+
+
+CREATE TYPE analytics.attribute_type AS ENUM ('MIN', 'MAX', 'AVG', 'VAL');
+
+CREATE TYPE analytics.threshold_mode AS ENUM ('LT', 'LE', 'GE', 'GT', 'NE', 'EQ');
+
+CREATE TYPE analytics.collector_type AS ENUM ('DOUBLE', 'MOLD');
+
+CREATE TYPE analytics.notify_trigger_mode AS ENUM ('INSTANT', 'ON_CHANGE', 'DISABLED');
+
+-- CREATE TYPE message_broker_type AS ENUM ('SENSLOG_ALERT');
+
+
+create table analytics.entity_source (
+    id SERIAL PRIMARY KEY NOT NULL,
+    unit_id BIGINT NOT NULL,
+    sensor_id BIGINT NOT NULL,
+    UNIQUE (unit_id, sensor_id)
+);
+
+alter table analytics.entity_source OWNER TO senslog;
+
+create table analytics.analytic_group (
+    id SERIAL PRIMARY KEY NOT NULL,
+    name VARCHAR(200) NOT NULL DEFAULT 'no_name',
+    time_interval INTEGER NOT NULL,     -- interval in seconds
+    persistence BOOLEAN NOT NULL,
+    collector_type analytics.collector_type
+);
+
+alter table analytics.analytic_group OWNER TO senslog;
+
+create table analytics.entity_source_to_analytic_group (
+    id SERIAL PRIMARY KEY NOT NULL,
+    entity_source_id BIGINT REFERENCES analytics.entity_source(id),
+    analytic_group_id INTEGER REFERENCES analytics.analytic_group(id),
+    time_created TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
+);
+
+alter table analytics.entity_source_to_analytic_group OWNER TO senslog;
+
+create table analytics.record (
+    id SERIAL PRIMARY KEY NOT NULL,
+    analytic_group_id INTEGER NOT NULL,
+    attribute_type analytics.attribute_type NOT NULL,
+    calculated_value DOUBLE PRECISION NOT NULL,
+    time_interval INTEGER NOT NULL,     -- interval in seconds
+    time_stamp TIMESTAMP WITH TIME ZONE NOT NULL,
+    time_created TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
+    UNIQUE (analytic_group_id, attribute_type, time_stamp)
+);
+
+alter table analytics.record OWNER TO senslog;
+
+
+create table analytics.threshold (
+   id SERIAL PRIMARY KEY NOT NULL,
+   analytic_group_id INTEGER NOT NULL,
+   notify_trigger_mode analytics.notify_trigger_mode NOT NULL,
+   attribute_type analytics.attribute_type NOT NULL,
+   process_on_fail BOOLEAN NOT NULL
+);
+
+alter table analytics.threshold OWNER TO senslog;
+
+create table analytics.threshold_rule (
+   id SERIAL PRIMARY KEY NOT NULL,
+   threshold_id INTEGER NOT NULL REFERENCES analytics.threshold(id),
+   threshold_mode analytics.threshold_mode NOT NULL,
+   threshold_value DOUBLE PRECISION NOT NULL
+);
+
+alter table analytics.threshold_rule OWNER TO senslog;
+
+-- create table analytics.alert_broker (
+--    id SERIAL PRIMARY KEY NOT NULL,
+--    broker_type message_broker_type NOT NULL,
+--    config jsonb NOT NULL
+-- );
+--
+-- alter table analytics.alert_broker OWNER TO senslog;
+--
+-- create table analytics.notify_to_msg_broker (
+--     id SERIAL PRIMARY KEY NOT NULL,
+--     threshold_id integer REFERENCES analytics.thresholds(id),
+--     message_broker_id integer REFERENCES analytics.alert_broker(id),
+--     properties json NOT NULL
+-- );
+--
+-- alter table analytics.notify_to_msg_broker OWNER TO senslog;

+ 15 - 0
local.dev.env

@@ -0,0 +1,15 @@
+# Servers
+SERVER_HTTP_PORT=8080
+
+# Database properties
+DATABASE_HOST=172.17.0.1
+DATABASE_PORT=5432
+DATABASE_NAME=analytics
+DATABASE_USER=analytics_app
+DATABASE_PASSWORD=analytics
+DATABASE_POOL_SIZE=5
+
+# Auth
+AUTH_KEYSTORE_PATH=keystore.jceks
+AUTH_KEYSTORE_TYPE=PKCS12
+AUTH_KEYSTORE_PASSWORD=SENSlog

+ 15 - 0
sql/config.sql

@@ -0,0 +1,15 @@
+-- Insert all sensors for analytics
+INSERT INTO analytics.sensors (unit_id, sensor_id)
+SELECT u.unit_id, s.sensor_id FROM export.sensors AS s
+JOIN export.units_to_sensors AS us ON us.sensor_id = s.sensor_id
+JOIN export.units AS u ON u.unit_id = us.unit_id;
+
+
+-- Create groups for all registered sensors
+INSERT INTO analytics.groups_interval(id, name, time_interval, persistence, collector_type)
+SELECT id, CONCAT(unit_id, '/', sensor_id), 3600, true, 'DOUBLE' FROM analytics.sensors;
+
+-- Map every sensor to its group (1:1 relation)
+INSERT INTO analytics.sensor_to_group(sensor_id, group_id)
+SELECT id, id FROM analytics.groups_interval;
+

+ 67 - 0
src/main/java/cz/senslog/analytics/MockData.java

@@ -0,0 +1,67 @@
+package cz.senslog.analytics;
+
+import cz.senslog.analytics.domain.*;
+import cz.senslog.analytics.utils.validator.NotifyTrigger;
+import io.vertx.core.json.JsonObject;
+
+import java.time.OffsetDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static cz.senslog.analytics.messaging.MessageBroker.Type.SENSLOG_ALERT;
+
+public class MockData {
+
+    public static final long uniqueSensorId = 100L;
+    public static final long groupId = 1L;
+    public static final long unitId = 100000;
+    public static final long sensorId = 10343;
+    public static final double observationValue = 25.3;
+
+    public static final Sensor sensor = new Sensor(uniqueSensorId, unitId, sensorId);
+    public static final Observation observation = new Observation(sensor, observationValue, OffsetDateTime.now());
+
+    public static final Group doubleGroup = Group.of(groupId, "Rostenice_Temperature", CollectorType.DOUBLE);
+
+    public static Map<Sensor, List<Group>> mockSensorToGroupConfig() {
+        return Map.of(sensor, List.of(doubleGroup));
+    }
+
+    public static List<Threshold> mockThresholdsConfigForSensors() {
+        return List.of(new Threshold(1, uniqueSensorId, NotifyTrigger.Type.INSTANT, true, AttributeName.VAL, List.of(
+                        new ThresholdRule("gt", 10.0)
+        )));
+    }
+
+    public static List<Threshold> mockThresholdsConfigForGroups(CollectorType type) {
+        if (Objects.requireNonNull(type) == CollectorType.DOUBLE) {
+            return List.of(new Threshold(2, doubleGroup.id(), NotifyTrigger.Type.INSTANT, true, AttributeName.MIN, List.of(
+                    new ThresholdRule("lt", 0.0),
+                    new ThresholdRule("gt", 10.0)))
+            );
+        }
+        return Collections.emptyList();
+    }
+
+    public static Map<Long, Group> mockGroupsConfig(CollectorType type) {
+        if (Objects.requireNonNull(type) == CollectorType.DOUBLE) {
+            return Map.of(doubleGroup.id(), doubleGroup);
+        }
+        return Collections.emptyMap();
+    }
+
+    public static List<MessageBrokerConfig> mockMessageBrokers() {
+        return List.of(
+                new MessageBrokerConfig(1L, SENSLOG_ALERT, JsonObject.of())
+        );
+    }
+
+    public static List<SourceToMessageBroker> mockSourceToMessageBroker() {
+        return List.of(
+                new SourceToMessageBroker(uniqueSensorId, 1L, JsonObject.of()),
+                new SourceToMessageBroker(groupId, 1L, JsonObject.of())
+        );
+    }
+}

+ 97 - 0
src/main/java/cz/senslog/analytics/app/Application.java

@@ -0,0 +1,97 @@
+package cz.senslog.analytics.app;
+
+import cz.senslog.analytics.module.api.Module;
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.PoolOptions;
+import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.SqlConnectOptions;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.Properties;
+
+public final class Application {
+
+    private static final Logger logger = LogManager.getLogger(Application.class);
+
+    private static final String DEFAULT_UNKNOWN = "unknown";
+    private static final long START_UP;
+    public static String COMPILED_VERSION;
+    public static String BUILD_VERSION;
+    public static String PROJECT_NAME;
+
+    static {
+        START_UP = System.currentTimeMillis();
+        try {
+            InputStream input = new FileInputStream("gradle.properties");
+            Properties prop = new Properties();
+            prop.load(input);
+            PROJECT_NAME = prop.getProperty("projectName", DEFAULT_UNKNOWN);
+            COMPILED_VERSION = prop.getProperty("projectVersion", DEFAULT_UNKNOWN);
+            BUILD_VERSION = prop.getProperty("buildVersion", DEFAULT_UNKNOWN);
+        } catch (IOException ex) {
+            logger.catching(ex);
+            terminate(ex.getMessage());
+        }
+    }
+
+    public static long uptime() {
+        return System.currentTimeMillis() - START_UP;
+    }
+
+    public static String uptimeFormatted() {
+        Duration duration = Duration.ofMillis(uptime());
+        long HH = duration.toHours();
+        long MM = duration.toMinutesPart();
+        long SS = duration.toSecondsPart();
+        return String.format("%02d:%02d:%02d", HH, MM, SS);
+    }
+
+    public static void terminate(String message) {
+        logger.error(message);
+        System.exit(1);
+    }
+
+    public static void start() {
+        logger.info("Starting app '{}', version '{}', build '{}'.", PROJECT_NAME, COMPILED_VERSION, BUILD_VERSION);
+
+        PropertyConfig config = PropertyConfig.getInstance();
+        DeploymentOptions options = new DeploymentOptions().setConfig(JsonObject.of(
+                "server", config.server(),
+                "auth", config.auth()
+        ));
+
+        PropertyConfig.Database dbConfig = config.dbConfig();
+        Pool dbPool = Pool.pool(new SqlConnectOptions()
+                .setPort(dbConfig.getPort())
+                .setHost(dbConfig.getHost())
+                .setDatabase(dbConfig.getDatabase())
+                .setUser(dbConfig.getUser())
+                .setPassword(dbConfig.getPassword()), new PoolOptions()
+                .setMaxSize(dbConfig.getPoolSize()));
+
+        dbPool.query("SELECT version()").execute().map(RowSet::iterator)
+                .map(it -> it.hasNext() ? it.next().getString(0) : null)
+                .onSuccess(version -> logger.info("Successful database connection to {}.", version))
+                .onFailure(fail -> terminate(fail.getMessage()));
+
+
+        AbstractVerticle[] modules = Module.createModules(dbPool);
+        Vertx.vertx().deployVerticle(VertxDeployer.deploy(modules), options, res -> {
+            if(res.succeeded()) {
+                logger.info("Deployment id is: {}", res.result());
+                logger.info("Started in {} second.", uptime() / 1000.0);
+            } else {
+                logger.error("Deployment failed! The reason is '{}'", res.cause().getMessage());
+            }
+        });
+    }
+}

+ 8 - 0
src/main/java/cz/senslog/analytics/app/Main.java

@@ -0,0 +1,8 @@
+package cz.senslog.analytics.app;
+
+public class Main {
+    public static void main(String[] args) {
+        Application.start();
+    }
+
+}

+ 152 - 0
src/main/java/cz/senslog/analytics/app/PropertyConfig.java

@@ -0,0 +1,152 @@
+package cz.senslog.analytics.app;
+
+import io.vertx.core.json.JsonObject;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+public final class PropertyConfig {
+    private static PropertyConfig INSTANCE = null;
+
+    private final HttpServer httpServerConfig;
+    private final Database dbConfig;
+
+    private final Auth authConfig;
+
+    public static PropertyConfig getInstance() {
+        return getInstance(System::getenv);
+    }
+
+    private static PropertyConfig getInstance(Function<String, String> getEnv) {
+        if (INSTANCE == null) {
+            INSTANCE = new PropertyConfig(
+                    new HttpServer(getEnv), new Database(getEnv), new Auth(getEnv)
+            );
+        }
+        return INSTANCE;
+    }
+
+    private PropertyConfig(HttpServer httpServer, Database dbConfig, Auth authConfig) {
+        this.httpServerConfig = httpServer;
+        this.dbConfig = dbConfig;
+        this.authConfig = authConfig;
+    }
+
+    public static class HttpServer {
+
+        private HttpServer(Function<String, String> getEnv) {
+            this.getEnv = getEnv;
+        }
+
+        private final Function<String, String> getEnv;
+
+        public int getPort() {
+            String portStr = getEnv.apply("SERVER_HTTP_PORT");
+            return portStr != null ? Integer.parseInt(portStr) : 8080;
+        }
+    }
+
+    public static class Database {
+
+        private Database(Function<String, String> getEnv) {
+            this.getEnv = getEnv;
+        }
+
+        private final Function<String, String> getEnv;
+
+        public int getPort() {
+            String portStr = getEnv.apply("DATABASE_PORT");
+            return portStr != null ? Integer.parseInt(portStr) : 5432;
+        }
+
+        public String getHost() {
+            String host = getEnv.apply("DATABASE_HOST");
+            return host != null ? host : "127.0.0.1";
+        }
+
+        public String getDatabase() {
+            String name = getEnv.apply("DATABASE_NAME");
+            return Objects.requireNonNull(name, "System environmental variable 'DATABASE_NAME' is not set.");
+        }
+
+        public String getUser() {
+            String user = getEnv.apply("DATABASE_USER");
+            return Objects.requireNonNull(user, "System environmental variable 'DATABASE_USER' is not set.");
+        }
+
+        public String getPassword() {
+            String passwd = getEnv.apply("DATABASE_PASSWORD");
+            return Objects.requireNonNull(passwd, "System environmental variable 'DATABASE_PASSWORD' is not set.");
+        }
+
+        public int getPoolSize() {
+            String poolSizeStr = getEnv.apply("DATABASE_POOL_SIZE");
+            return poolSizeStr != null ? Integer.parseInt(poolSizeStr) : 5;
+        }
+    }
+
+    public static class Auth {
+
+        private Auth(Function<String, String> getEnv) {
+            this.getEnv = getEnv;
+        }
+
+        private final Function<String, String> getEnv;
+
+        public boolean getDisabled() {
+            return Boolean.parseBoolean(getEnv.apply("AUTH_DISABLED"));
+        }
+        public String getKeyStorePath() {
+            String user = getEnv.apply("AUTH_KEYSTORE_PATH");
+            return Objects.requireNonNull(user, "System environmental variable 'AUTH_KEYSTORE_PATH' is not set.");
+        }
+
+        public String getKeyStoreType() {
+            String user = getEnv.apply("AUTH_KEYSTORE_TYPE");
+            return Objects.requireNonNull(user, "System environmental variable 'AUTH_KEYSTORE_TYPE' is not set.");
+        }
+
+        public String getKeyStorePassword() {
+            String user = getEnv.apply("AUTH_KEYSTORE_PASSWORD");
+            return Objects.requireNonNull(user, "System environmental variable 'AUTH_KEYSTORE_PASSWORD' is not set.");
+        }
+    }
+
+    public HttpServer httpServerConfig() {
+        return httpServerConfig;
+    }
+
+    public Database dbConfig() {
+        return dbConfig;
+    }
+
+    public Auth authConfig() {
+        return authConfig;
+    }
+
+    public JsonObject server() {
+        return JsonObject.of(
+                "http.port", httpServerConfig.getPort()
+        );
+    }
+
+    public JsonObject auth() {
+        return JsonObject.of(
+                "keystore.path", authConfig.getKeyStorePath(),
+                "keystore.type", authConfig.getKeyStoreType(),
+                "keystore.password", authConfig.getKeyStorePassword(),
+                "disabled", authConfig.getDisabled()
+        );
+    }
+
+    public JsonObject db() {
+        return JsonObject.of(
+            "port", dbConfig.getPort(),
+            "host", dbConfig.getHost(),
+            "database", dbConfig.getDatabase(),
+            "user", dbConfig.getUser(),
+            "password", dbConfig.getPassword(),
+            "pool.size", dbConfig.getPoolSize()
+        );
+    }
+}

+ 94 - 0
src/main/java/cz/senslog/analytics/app/VertxDeployer.java

@@ -0,0 +1,94 @@
+package cz.senslog.analytics.app;
+
+import cz.senslog.analytics.domain.ThresholdViolationNotification;
+import cz.senslog.analytics.domain.Observation;
+import io.vertx.core.*;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.eventbus.MessageCodec;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class VertxDeployer extends AbstractVerticle {
+
+    private static final Logger logger = LogManager.getLogger(VertxDeployer.class);
+
+    private final AbstractVerticle[] verticles;
+
+    private VertxDeployer(AbstractVerticle[] verticles) {
+        this.verticles = verticles;
+    }
+
+    public static VertxDeployer deploy(AbstractVerticle... verticles) {
+        return new VertxDeployer(verticles);
+    }
+
+    @Override
+    public void start(Promise<Void> startPromise) {
+        vertx.eventBus().registerDefaultCodec(Observation.class, new IdentityCodec<>(Observation.class));
+        vertx.eventBus().registerDefaultCodec(ThresholdViolationNotification.class, new IdentityCodec<>(ThresholdViolationNotification.class));
+
+
+        List<Future<Void>> futureModules = new ArrayList<>(verticles.length);
+        for (AbstractVerticle v : verticles) {
+            DeploymentOptions options = new DeploymentOptions()
+                    .setThreadingModel(ThreadingModel.WORKER)
+                    .setConfig(config());
+            futureModules.add(deployHelper(vertx, options, v));
+        }
+        Future.all(futureModules)
+                .onSuccess(v -> startPromise.complete())
+                .onFailure(startPromise::fail);
+    }
+
+    private static Future<Void> deployHelper(Vertx vertx, DeploymentOptions options, AbstractVerticle verticle) {
+        logger.info("Deploying module: " + vertx.getClass().getName());
+        final Promise<Void> promise = Promise.promise();
+        vertx.deployVerticle(verticle, options, res -> {
+            if(res.failed()){
+                logger.error("Module '{}' was not deployed.", verticle.getClass().getSimpleName());
+                logger.catching(res.cause());
+                promise.fail(res.cause());
+            } else {
+                logger.info("Module '{}' was deployed successfully.", verticle.getClass().getSimpleName());
+                promise.complete();
+            }
+        });
+        return promise.future();
+    }
+
+    static class IdentityCodec<T> implements MessageCodec<T, T> {
+        private final Class<T> aClass;
+
+        public IdentityCodec(Class<T> aClass) {
+            this.aClass = aClass;
+        }
+
+        @Override
+        public void encodeToWire(Buffer buffer, T o) {
+
+        }
+
+        @Override
+        public T decodeFromWire(int pos, Buffer buffer) {
+            return null;
+        }
+
+        @Override
+        public T transform(T o) {
+            return o;
+        }
+
+        @Override
+        public String name() {
+            return aClass.getName() + "Codec";
+        }
+
+        @Override
+        public byte systemCodecID() {
+            return -1;
+        }
+    }
+}

+ 2 - 2
src/main/java/cz/senslog/analyzer/domain/AttributeValue.java → src/main/java/cz/senslog/analytics/domain/AttributeName.java

@@ -1,6 +1,6 @@
-package cz.senslog.analyzer.domain;
+package cz.senslog.analytics.domain;
 
-public enum AttributeValue {
+public enum AttributeName {
 
     MIN,
     MAX,

+ 6 - 0
src/main/java/cz/senslog/analytics/domain/CollectorType.java

@@ -0,0 +1,6 @@
+package cz.senslog.analytics.domain;
+
+public enum CollectorType {
+    DOUBLE,
+    MOLD,
+}

+ 145 - 0
src/main/java/cz/senslog/analytics/domain/DoubleStatistics.java

@@ -0,0 +1,145 @@
+package cz.senslog.analytics.domain;
+
+import java.time.OffsetDateTime;
+import java.util.Objects;
+import java.util.stream.DoubleStream;
+
+public class DoubleStatistics implements TimeSeriesDatasource {
+
+    private long count;
+    private double sum;
+    private double sumCompensation;
+    private double simpleSum;
+    private double min = 1.0D / 0.0;
+    private double max = -1.0D / 0.0;
+    private final OffsetDateTime timestamp;
+
+    private final long sourceId;
+
+    public static DoubleStatistics init(long sourceId, OffsetDateTime timestamp) {
+        return new DoubleStatistics(sourceId, timestamp);
+    }
+
+    private DoubleStatistics(long sourceId, OffsetDateTime timestamp) {
+        this.sourceId = sourceId;
+        this.timestamp = timestamp;
+    }
+
+    public DoubleStatistics(DoubleStatistics statistics) {
+        this(statistics.sourceId, statistics.timestamp);
+    }
+
+    public DoubleStatistics(long sourceId, DoubleStatistics statistics, OffsetDateTime timestamp) {
+        this(sourceId, statistics.count, statistics.min, statistics.max, statistics.sum, timestamp);
+    }
+
+    public DoubleStatistics(long sourceId, long count, double min, double max, double sum, OffsetDateTime timestamp) {
+        this.sourceId = sourceId;
+        this.timestamp = timestamp;
+        if (count < 0L) {
+            throw new IllegalArgumentException("Negative count value");
+        } else {
+            if (count > 0L) {
+                if (min > max) {
+                    throw new IllegalArgumentException("Minimum greater than maximum");
+                }
+
+                long nCount = DoubleStream.of(min, max, sum).filter(Double::isNaN).count();
+                if (nCount > 0L && nCount < 3L) {
+                    throw new IllegalArgumentException("Some, not all, of the minimum, maximum, or sum is NaN");
+                }
+
+                this.count = count;
+                this.sum = sum;
+                this.simpleSum = sum;
+                this.sumCompensation = 0.0D;
+                this.min = min;
+                this.max = max;
+            }
+        }
+    }
+
+    private void sumWithCompensation(double value) {
+        double tmp = value - this.sumCompensation;
+        double velVel = this.sum + tmp;
+        this.sumCompensation = velVel - this.sum - tmp;
+        this.sum = velVel;
+    }
+
+    public final long count() {
+        return this.count;
+    }
+
+    public final double sum() {
+        double tmp = this.sum + this.sumCompensation;
+        return Double.isNaN(tmp) && Double.isInfinite(this.simpleSum) ? this.simpleSum : tmp;
+    }
+
+    public final double min() {
+        return this.min;
+    }
+
+    public final double max() {
+        return this.max;
+    }
+
+    public final double average() {
+        return this.count() > 0L ? this.sum() / (double)this.count() : 0.0D;
+    }
+
+    public OffsetDateTime timestamp() {
+        return timestamp;
+    }
+
+    private boolean acceptSource(long sourceId) {
+        return sourceId == datasourceId();
+    }
+
+    public boolean accept(DoubleStatistics other) {
+        if (acceptSource(other.datasourceId())) {
+            this.count += other.count;
+            this.simpleSum += other.simpleSum;
+            this.sumWithCompensation(other.sum);
+            this.sumWithCompensation(other.sumCompensation);
+            this.min = Math.min(this.min, other.min);
+            this.max = Math.max(this.max, other.max);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean accept(long sourceId, double value) {
+        if (acceptSource(sourceId)) {
+            ++this.count;
+            this.simpleSum += value;
+            this.sumWithCompensation(value);
+            this.min = Math.min(this.min, value);
+            this.max = Math.max(this.max, value);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DoubleStatistics that = (DoubleStatistics) o;
+        return datasourceId() == that.datasourceId() &&
+                min() == that.min() &&
+                max() == that.max() &&
+                sum() == that.sum() &&
+                average() == that.average() &&
+                count() == that.count();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(datasourceId(), min(), max(), average(), sum(), count());
+    }
+
+    @Override
+    public long datasourceId() {
+        return sourceId;
+    }
+}

+ 16 - 0
src/main/java/cz/senslog/analytics/domain/Group.java

@@ -0,0 +1,16 @@
+package cz.senslog.analytics.domain;
+
+public record Group(long id, String name, int interval, boolean persistence, CollectorType type) {
+
+    public Group(long id, CollectorType type) {
+        this(id, null, -1, false, type);
+    }
+
+    public static Group of(long id) {
+        return new Group(id, null, -1, false, null);
+    }
+
+    public static Group of(long id, String name, CollectorType type) {
+        return new Group(id, name, 0, false, type);
+    }
+}

+ 6 - 0
src/main/java/cz/senslog/analytics/domain/MessageBrokerConfig.java

@@ -0,0 +1,6 @@
+package cz.senslog.analytics.domain;
+
+import cz.senslog.analytics.messaging.MessageBroker;
+import io.vertx.core.json.JsonObject;
+
+public record MessageBrokerConfig(long id, MessageBroker.Type senderType, JsonObject config) {}

+ 11 - 0
src/main/java/cz/senslog/analytics/domain/Observation.java

@@ -0,0 +1,11 @@
+package cz.senslog.analytics.domain;
+
+import java.time.OffsetDateTime;
+
+public record Observation(Sensor source, double value, OffsetDateTime timestamp) implements TimeSeriesDatasource {
+
+    @Override
+    public long datasourceId() {
+        return source.id();
+    }
+}

+ 50 - 0
src/main/java/cz/senslog/analytics/domain/Sensor.java

@@ -0,0 +1,50 @@
+package cz.senslog.analytics.domain;
+
+
+import java.util.Objects;
+
+public record Sensor(long id, long unitId, long sensorId, long groupId) {
+
+    public static Sensor empty() {
+        return new Sensor(-1, -1, -1, -1);
+    }
+
+    public Sensor(long unitId, long  sensorId) {
+        this(-1, unitId, sensorId, -1);
+    }
+
+    public Sensor(long id, long unitId, long  sensorId) {
+        this(id, unitId, sensorId, -1);
+    }
+
+    public Sensor(Sensor sensor, long groupId) {
+        this(sensor.id(), sensor.unitId(), sensor.sensorId(), groupId);
+    }
+
+    public boolean isEmpty() {
+        return this.equals(empty());
+    }
+
+    public boolean isNotEmpty() {
+        return !isEmpty();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Sensor sensor = (Sensor) o;
+        return unitId == sensor.unitId &&
+                sensorId == sensor.sensorId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(unitId, sensorId);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("[%d] %d-%d", id, unitId, sensorId);
+    }
+}

+ 6 - 0
src/main/java/cz/senslog/analytics/domain/SourceToMessageBroker.java

@@ -0,0 +1,6 @@
+package cz.senslog.analytics.domain;
+
+import io.vertx.core.json.JsonObject;
+
+public record SourceToMessageBroker(long thresholdId, long messageBrokerId, JsonObject properties) {
+}

+ 6 - 0
src/main/java/cz/senslog/analytics/domain/StatisticRecord.java

@@ -0,0 +1,6 @@
+package cz.senslog.analytics.domain;
+
+import java.time.OffsetDateTime;
+
+public record StatisticRecord(long id, long groupId, String valueAttribute, double recordValue, int timeInterval, OffsetDateTime timestamp) {
+}

+ 9 - 0
src/main/java/cz/senslog/analytics/domain/Threshold.java

@@ -0,0 +1,9 @@
+package cz.senslog.analytics.domain;
+
+
+import cz.senslog.analytics.utils.validator.NotifyTrigger;
+
+import java.util.List;
+
+public record Threshold(long id, long groupId, NotifyTrigger.Type notifyTriggerType, boolean allowProcess, AttributeName attribute, List<ThresholdRule> rules) {
+}

+ 4 - 0
src/main/java/cz/senslog/analytics/domain/ThresholdRule.java

@@ -0,0 +1,4 @@
+package cz.senslog.analytics.domain;
+
+public record ThresholdRule(String mode, Double value) {
+}

+ 6 - 0
src/main/java/cz/senslog/analytics/domain/ThresholdViolationNotification.java

@@ -0,0 +1,6 @@
+package cz.senslog.analytics.domain;
+
+import java.time.OffsetDateTime;
+
+public record ThresholdViolationNotification(String moduleName, long groupId, String sourceName, ValidationResult[] violatedData, OffsetDateTime timestamp) {
+}

+ 10 - 0
src/main/java/cz/senslog/analytics/domain/TimeSeriesDatasource.java

@@ -0,0 +1,10 @@
+package cz.senslog.analytics.domain;
+
+import java.time.OffsetDateTime;
+
+public interface TimeSeriesDatasource {
+
+    long datasourceId();
+
+    OffsetDateTime timestamp();
+}

+ 43 - 0
src/main/java/cz/senslog/analytics/domain/ValidationResult.java

@@ -0,0 +1,43 @@
+package cz.senslog.analytics.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ValidationResult {
+
+    public record Record(String mode, double thresholdValue) {}
+
+    private final double validatedValue;
+    private final AttributeName attribute;
+    private final List<Record> records;
+
+    public ValidationResult(AttributeName attribute, double validatedValue) {
+        this.attribute = attribute;
+        this.validatedValue = validatedValue;
+        this.records = new ArrayList<>();
+    }
+
+    public boolean isValid() {
+        return records.isEmpty();
+    }
+
+    public boolean isNotValid() {
+        return !isValid();
+    }
+
+    public List<Record> records() {
+        return records;
+    }
+
+    public double validatedValue() {
+        return validatedValue;
+    }
+
+    public AttributeName attribute() {
+        return attribute;
+    }
+
+    public void addRecord(String mode, double thresholdValue) {
+        records.add(new Record(mode, thresholdValue));
+    }
+}

+ 7 - 0
src/main/java/cz/senslog/analytics/domain/ViolationReport.java

@@ -0,0 +1,7 @@
+package cz.senslog.analytics.domain;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+public record ViolationReport(long sourceId, OffsetDateTime timestamp, ValidationResult[] violatedData) {
+}

+ 63 - 0
src/main/java/cz/senslog/analytics/messaging/MessageBroker.java

@@ -0,0 +1,63 @@
+package cz.senslog.analytics.messaging;
+
+import cz.senslog.analytics.domain.ValidationResult;
+import cz.senslog.analytics.domain.ThresholdViolationNotification;
+import io.vertx.core.json.JsonObject;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+@FunctionalInterface
+public interface MessageBroker {
+    enum Type {
+        SENSLOG_ALERT   (SensLogAlertMessageBroker::new),
+
+        ;
+        private final Function<JsonObject, MessageBroker> constructCreator;
+
+        Type(Function<JsonObject, MessageBroker> constructCreator) {
+            this.constructCreator = constructCreator;
+        }
+
+        public MessageBroker createBroker(JsonObject config) {
+            return constructCreator.apply(config);
+        }
+    }
+
+    void send(ThresholdViolationNotification notification, JsonObject recipients);
+
+    default String createMessage(ThresholdViolationNotification n) {
+        ValidationResult[] violatedData = n.violatedData();
+        if (violatedData.length == 0) {
+            return null;
+        }
+        StringBuilder b = new StringBuilder();
+        b.append(createLineMessage(n.moduleName(), n.sourceName(), violatedData[0], n.timestamp()));
+        for (int i = 1; i < violatedData.length; i++) {
+            b.append("\n").append(createLineMessage(n.moduleName(), n.sourceName(), violatedData[i], n.timestamp()));
+        }
+        return b.toString();
+    }
+
+    default String createLineMessage(String moduleName, String sourceName, ValidationResult validationResult, OffsetDateTime timestamp) {
+        final String delimiter = " ";
+        String sourceStr = moduleName + delimiter + sourceName;
+        String attrValue = String.format("%s(%.2f)", validationResult.attribute(), validationResult.validatedValue());
+        String timestampStr = "at" + delimiter + timestamp.format(DateTimeFormatter.ISO_DATE_TIME);
+        List<ValidationResult.Record> records = validationResult.records();
+        if (records.isEmpty()) {
+            return sourceStr + delimiter + timestampStr;
+        }
+        Iterator<ValidationResult.Record> recIter = records.iterator();
+        ValidationResult.Record rec1 = recIter.next();
+        StringBuilder rulesStr = new StringBuilder(String.format("%s %s %.2f", attrValue, rec1.mode(), rec1.thresholdValue()));
+        while (recIter.hasNext()) {
+            ValidationResult.Record rec = recIter.next();
+            rulesStr.append(String.format("&& %s %s %.2f", attrValue, rec.mode(), rec.thresholdValue()));
+        }
+        return sourceStr + delimiter + rulesStr + delimiter + timestampStr;
+    }
+}

+ 21 - 0
src/main/java/cz/senslog/analytics/messaging/SensLogAlertMessageBroker.java

@@ -0,0 +1,21 @@
+package cz.senslog.analytics.messaging;
+
+import cz.senslog.analytics.domain.ThresholdViolationNotification;
+import io.vertx.core.json.JsonObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class SensLogAlertMessageBroker implements MessageBroker {
+    private static final Logger logger = LogManager.getLogger(SensLogAlertMessageBroker.class);
+
+    private final JsonObject config;
+
+    public SensLogAlertMessageBroker(JsonObject config) {
+        this.config = config;
+    }
+
+    @Override
+    public void send(ThresholdViolationNotification notification, JsonObject recipients) {
+        logger.info("SensLog Alert: {}", createMessage(notification));
+    }
+}

+ 62 - 0
src/main/java/cz/senslog/analytics/module/DoubleStatisticsModule.java

@@ -0,0 +1,62 @@
+package cz.senslog.analytics.module;
+
+import cz.senslog.analytics.domain.*;
+import cz.senslog.analytics.module.api.CollectorModule;
+import cz.senslog.analytics.repository.ConfigurationRepository;
+import cz.senslog.analytics.repository.AnalyticsDataRepository;
+import cz.senslog.analytics.utils.validator.ThresholdChecker;
+import cz.senslog.analytics.utils.validator.Validator;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class DoubleStatisticsModule extends CollectorModule {
+
+    private static final Logger logger = LogManager.getLogger(DoubleStatisticsModule.class);
+
+    private static final Validator<DoubleStatistics> validator;
+
+    private ThresholdChecker<DoubleStatistics> thresholdChecker;
+
+    static {
+        validator = Validator.<DoubleStatistics>create()
+                .addMapping(AttributeName.MIN, s -> s::min)
+                .addMapping(AttributeName.MAX, s -> s::max)
+                .addMapping(AttributeName.AVG, s -> s::average);
+    }
+
+    public DoubleStatisticsModule(ConfigurationRepository configRepo, AnalyticsDataRepository statisticsRep) {
+        super(CollectorType.DOUBLE, configRepo, statisticsRep);
+    }
+
+    @Override
+    protected void postConfigure() {
+        thresholdChecker = new ThresholdChecker<>(thresholds(), validator, this::notifyIfViolation);
+    }
+
+    private void notifyIfViolation(ViolationReport report) {
+        Optional<Group> source = Optional.ofNullable(groups().get(report.sourceId()));
+        String sourceName = source.map(Group::name).orElse("unknown");
+        notify(new ThresholdViolationNotification(type().name(), report.sourceId(), sourceName, report.violatedData(), report.timestamp()));
+    }
+
+    private Stream<StatisticRecord> mapToStatisticRecord(DoubleStatistics st) {
+        Group group = groups().get(st.datasourceId());
+        return Stream.of(
+            new StatisticRecord(0, st.datasourceId(), AttributeName.MIN.name(), st.min(), group.interval(), st.timestamp()),
+            new StatisticRecord(0, st.datasourceId(), AttributeName.AVG.name(), st.average(), group.interval(), st.timestamp()),
+            new StatisticRecord(0, st.datasourceId(), AttributeName.MAX.name(), st.max(), group.interval(), st.timestamp())
+        );
+    }
+
+    @Override
+    protected void handle(Collection<DoubleStatistics> aggregations) {
+        aggregations.stream()
+                .filter(thresholdChecker.check())
+                .flatMap(this::mapToStatisticRecord)
+                .forEach(this::persist);
+    }
+}

+ 26 - 0
src/main/java/cz/senslog/analytics/module/MoldAnalysisModule.java

@@ -0,0 +1,26 @@
+package cz.senslog.analytics.module;
+
+import cz.senslog.analytics.domain.CollectorType;
+import cz.senslog.analytics.domain.DoubleStatistics;
+import cz.senslog.analytics.module.api.CollectorModule;
+import cz.senslog.analytics.repository.ConfigurationRepository;
+import cz.senslog.analytics.repository.AnalyticsDataRepository;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Collection;
+
+public class MoldAnalysisModule extends CollectorModule {
+
+    private static final Logger logger = LogManager.getLogger(MoldAnalysisModule.class);
+
+    public MoldAnalysisModule(ConfigurationRepository configRepo, AnalyticsDataRepository statisticsRep) {
+        super(CollectorType.MOLD, configRepo, statisticsRep);
+    }
+
+    @Override
+    protected void handle(Collection<DoubleStatistics> aggregations) {
+        // TODO algorithm for mold analysis
+        persist(null);
+    }
+}

+ 62 - 0
src/main/java/cz/senslog/analytics/module/NotificationModule.java

@@ -0,0 +1,62 @@
+package cz.senslog.analytics.module;
+
+import cz.senslog.analytics.domain.MessageBrokerConfig;
+import cz.senslog.analytics.domain.SourceToMessageBroker;
+import cz.senslog.analytics.domain.ThresholdViolationNotification;
+import cz.senslog.analytics.module.api.SimpleModule;
+import cz.senslog.analytics.messaging.MessageBroker;
+import cz.senslog.analytics.repository.ConfigurationRepository;
+import io.vertx.core.json.JsonObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+public final class NotificationModule extends SimpleModule {
+
+    private static final Logger logger = LogManager.getLogger(NotificationModule.class);
+
+    private record SourceSender(MessageBroker messageBroker, JsonObject properties) {
+        public void send(ThresholdViolationNotification notification) {
+            messageBroker.send(notification, properties);
+        }
+    }
+
+    private static final SourceSender DEFAULT_SOURCE_SENDER = new SourceSender((n, props) ->
+            logger.warn("No notification sender for source {}.", n.groupId()), null);
+
+    private final ConfigurationRepository repo;
+
+    private Map<Long, SourceSender> senders;
+
+    public NotificationModule(ConfigurationRepository repo) {
+        this.repo = repo;
+        this.senders = Collections.emptyMap();
+    }
+
+    @Override
+    public void configure() {
+        repo.loadMessageBrokers().onSuccess(brokers -> {
+            Map<Long, MessageBroker> msgBrokerMap = new HashMap<>(brokers.size());
+            for (MessageBrokerConfig brokerConfig : brokers) {
+                msgBrokerMap.put(brokerConfig.id(), brokerConfig.senderType().createBroker(brokerConfig.config()));
+            }
+            repo.loadSourceToMessageBrokerMapping().onSuccess(mapping -> {
+                senders = new HashMap<>(mapping.size());
+                for (SourceToMessageBroker sToMsgConfig : mapping) {
+                    MessageBroker msgBroker = msgBrokerMap.getOrDefault(sToMsgConfig.messageBrokerId(), DEFAULT_SOURCE_SENDER.messageBroker);
+                    senders.put(sToMsgConfig.thresholdId(), new SourceSender(msgBroker, sToMsgConfig.properties()));
+                }
+            }).onFailure(logger::catching);
+        }).onFailure(logger::catching);
+    }
+
+    @Override
+    public void run() {
+        vertx.eventBus().<ThresholdViolationNotification>consumer(id(),
+                msg -> senders.getOrDefault(msg.body().groupId(), DEFAULT_SOURCE_SENDER).send(msg.body()));
+    }
+}

+ 108 - 0
src/main/java/cz/senslog/analytics/module/ObservationReceiverModule.java

@@ -0,0 +1,108 @@
+package cz.senslog.analytics.module;
+
+import cz.senslog.analytics.domain.*;
+import cz.senslog.analytics.repository.ConfigurationRepository;
+import cz.senslog.analytics.utils.validator.ThresholdChecker;
+import cz.senslog.analytics.module.api.Module;
+import cz.senslog.analytics.module.api.ModuleDescriptor;
+import cz.senslog.analytics.module.api.SimpleModule;
+import cz.senslog.analytics.utils.validator.Validator;
+import cz.senslog.analytics.utils.Tuple;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static java.util.Collections.emptyMap;
+
+public class ObservationReceiverModule extends SimpleModule {
+
+    private static final Logger logger = LogManager.getLogger(ObservationReceiverModule.class);
+
+
+    private static final String MODULE_NAME = "RECEIVE";
+    private static final String DEFAULT_SOURCE_NAME = "unknown";
+
+    private final ConfigurationRepository repo;
+
+    private Map<Sensor, List<Group>> sensorToGroupMap;
+    private ThresholdChecker<Observation> thresholdChecker;
+
+    private static final Validator<Observation> VALIDATOR;
+
+    static {
+        VALIDATOR = Validator.<Observation>create()
+                .addMapping(AttributeName.VAL, o -> o::value);
+    }
+
+    private AtomicInteger counter = new AtomicInteger(1);
+
+    public ObservationReceiverModule(ConfigurationRepository repo) {
+        this.repo = repo;
+        this.sensorToGroupMap = emptyMap();
+        this.thresholdChecker = ThresholdChecker.disabled();
+    }
+
+    @Override
+    public void configure() {
+        repo.loadSensorToGroupsMapping()
+                .onSuccess(data -> sensorToGroupMap = data)
+                .onFailure(logger::catching);
+
+        repo.loadSensorThresholds()
+                .onSuccess(data -> thresholdChecker = new ThresholdChecker<>(data, VALIDATOR, this::notifyIfViolated))
+                .onFailure(logger::catching);
+    }
+
+    private void notifyIfViolated(ViolationReport report) {
+        Optional<Sensor> sourceOpt = sensorToGroupMap.keySet().stream().filter(s -> s.id() == report.sourceId()).findFirst();
+        String sourceName = sourceOpt.map(s -> String.format("%d/%d", s.unitId(), s.sensorId())).orElse(DEFAULT_SOURCE_NAME);
+        notify(new ThresholdViolationNotification(MODULE_NAME, report.sourceId(), sourceName, report.violatedData(), report.timestamp()));
+    }
+
+    @Override
+    public void run() {
+        // consume -> flatMap -> thsChecker -> filter(collector_type != null) -> publish
+        //-> consume -> map(collector_type == null) -> thsChecker -> map(collector_type != null) -> publish
+        eventBus().<Observation>consumer(id(), h -> Stream.of(h.body())
+                .flatMap(mappingToGroups())
+                .filter(o -> thresholdChecker.check().test(o.getItem2()))
+                .forEach(o -> o.getItem1().ifPresent(mId -> eventBus().publish(mId, o.getItem2())))
+        );
+        
+//        eventBus().<Observation>consumer(id(), v -> Stream.of(v.body())
+//                .filter(o -> sensorToGroupMap.containsKey(o.source()))
+//                .filter(thresholdChecker.check())
+//                .flatMap(mapToGroups())
+//                .forEach(t -> eventBus().publish(t.getItem1(), t.getItem2())));
+    }
+
+    private Function<Observation, Stream<Tuple<Optional<String>, Observation>>> mappingToGroups() {
+        return o -> {
+//            System.out.println(counter.getAndIncrement());
+            if (sensorToGroupMap.containsKey(o.source())) {
+                return sensorToGroupMap.get(o.source()).stream()
+                        .map(g -> Tuple.of(collectorTypeToId(g), new Observation(new Sensor(o.source(), g.id()), o.value(), o.timestamp())));
+            } else {
+                return Stream.empty();
+            }
+        };
+    }
+
+//    private Function<Observation, Stream<Tuple<Optional<String>, Observation>>> mapToGroups() {
+//        return o -> sensorToGroupMap.getOrDefault(o.source(), emptyList()) // TODO map directly to thresholdId
+//                .stream()
+//                .map(g -> Tuple.of(
+//                        collectorTypeToId(g),
+//                        Observation.of(new Sensor(o.source(), g.id()), o.value(), o.timestamp()))
+//                ).filter(t -> t.getItem1().isPresent()).map(t -> Tuple.of(t.getItem1().get(), t.getItem2()));
+//    }
+
+    private static Optional<String> collectorTypeToId(Group g) {
+        ModuleDescriptor descriptor = Module.collectorOf(g.type());
+        return descriptor != null ? Optional.of(descriptor.id()) : Optional.empty();
+    }
+}

+ 52 - 0
src/main/java/cz/senslog/analytics/module/ScheduleDBLoaderModule.java

@@ -0,0 +1,52 @@
+package cz.senslog.analytics.module;
+
+import cz.senslog.analytics.module.api.Module;
+import cz.senslog.analytics.module.api.SimpleModule;
+import cz.senslog.analytics.repository.ConfigurationRepository;
+import cz.senslog.analytics.repository.SensLogDataRepository;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.List;
+
+public class ScheduleDBLoaderModule extends SimpleModule {
+
+    private static final Logger logger = LogManager.getLogger(ScheduleDBLoaderModule.class);
+
+    private final ConfigurationRepository configRepo;
+
+    private final SensLogDataRepository sensLogRepo;
+
+    public ScheduleDBLoaderModule(ConfigurationRepository configRepo, SensLogDataRepository dataRepo) {
+        this.configRepo = configRepo;
+        this.sensLogRepo = dataRepo;
+    }
+
+    @Override
+    public void configure() {
+        // TODO load tasks from DB and creat timers
+        List<Object> tasks = List.of(
+                new Object()
+        );
+
+    }
+
+    @Override
+    public void run() throws Exception {
+        // TODO calculate FROM & TO
+
+//        final String busId = Module.of(ObservationReceiverModule.class).id();
+//        sensLogRepo.cursorAllObservations(1000, o -> eventBus().publish(busId, o), logger::catching);
+
+
+//        vertx.setPeriodic(1000, 216000, l -> sensLogRepo.loadObservations(null, null)
+//                .onSuccess(data -> data.forEach(o -> eventBus().publish(busId, o)))
+//                .onFailure(logger::catching)
+//        );
+
+
+//        vertx.setPeriodic(1000, (l) -> sensLogRepo.loadObservations(null, null).onSuccess(data -> data
+//                .forEach(o -> eventBus().publish(busId, o))
+//        ).onFailure(logger::catching));
+    }
+}

+ 57 - 0
src/main/java/cz/senslog/analytics/module/api/CollectedStatistics.java

@@ -0,0 +1,57 @@
+package cz.senslog.analytics.module.api;
+
+import cz.senslog.analytics.domain.DoubleStatistics;
+import cz.senslog.analytics.domain.Group;
+import cz.senslog.analytics.domain.Sensor;
+import cz.senslog.analytics.utils.DateTrunc;
+
+import java.time.OffsetDateTime;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class CollectedStatistics {
+
+    private final Group group;
+    private final OffsetDateTime startTime, endTime;
+    private final Map<Long, DoubleStatistics> statistics;
+
+    public static CollectedStatistics init(Group group, OffsetDateTime timestamp) {
+        return new CollectedStatistics(group, DateTrunc.trunc(timestamp, group.interval()));
+    }
+
+    private CollectedStatistics(Group group, OffsetDateTime startTime) {
+        this.group = group;
+        this.startTime = startTime;
+        this.endTime = startTime.plusSeconds(group.interval());
+        this.statistics = new HashMap<>();
+    }
+
+    public OffsetDateTime startTime() {
+        return startTime;
+    }
+
+    public OffsetDateTime endTime() {
+        return endTime;
+    }
+
+    public Group group() {
+        return group;
+    }
+
+    public Collection<DoubleStatistics> statistics() {
+        return statistics.values();
+    }
+
+    public DoubleStatistics accept(Sensor source, double value) {
+        long sourceId = source.groupId();
+        DoubleStatistics st = statistics.computeIfAbsent(sourceId, s -> DoubleStatistics.init(s, startTime));
+        st.accept(sourceId, value);
+        return st;
+    }
+
+    public boolean readyToProcess() {
+        return false;
+    }
+}

+ 136 - 0
src/main/java/cz/senslog/analytics/module/api/CollectorModule.java

@@ -0,0 +1,136 @@
+package cz.senslog.analytics.module.api;
+
+import cz.senslog.analytics.domain.*;
+import cz.senslog.analytics.repository.ConfigurationRepository;
+import cz.senslog.analytics.repository.AnalyticsDataRepository;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.util.*;
+
+import static cz.senslog.analytics.utils.TimeUtils.minToMillis;
+import static cz.senslog.analytics.utils.TimeUtils.minToSec;
+
+public abstract class CollectorModule extends SimpleModule {
+
+    private static final Logger logger = LogManager.getLogger(CollectorModule.class);
+
+    private static final int MAX_WAITING_TO_HARVEST_MIN = 10;
+
+    private final CollectorType type;
+
+    protected final ConfigurationRepository configRepo;
+
+    private final AnalyticsDataRepository statisticsRepo;
+
+    private Map<Long, List<CollectedStatistics>> collectedStats;
+    private Map<Long, Group> groupMap;
+    private List<Threshold> thresholds;
+
+    private Instant lastHarvesting;
+
+    protected CollectorModule(CollectorType type, ConfigurationRepository configRepo, AnalyticsDataRepository statisticsRepo) {
+        this.type = type;
+        this.configRepo = configRepo;
+        this.statisticsRepo = statisticsRepo;
+
+        this.groupMap = Collections.emptyMap();
+        this.thresholds = Collections.emptyList();
+        this.collectedStats = Collections.emptyMap();
+    }
+
+    protected abstract void handle(Collection<DoubleStatistics> aggregations);
+
+    public final CollectorType type() {
+        return type;
+    }
+
+    protected void postConfigure() {}
+
+    @Override
+    protected final void configure() {
+        lastHarvesting = Instant.now();
+        configRepo.loadGroupsByCollectorType(type).onSuccess(groups -> {
+            collectedStats = new HashMap<>(groups.size());
+            groupMap = groups;
+
+            configRepo.loadGroupThresholds(type).onSuccess(data -> {
+                thresholds = data;
+                postConfigure();
+            }).onFailure(logger::catching);
+        }).onFailure(logger::catching);
+        long period = minToMillis(MAX_WAITING_TO_HARVEST_MIN);
+        vertx.setPeriodic(period, period, l -> {
+           if (overtakeHarvestTime()) {
+               harvestStatistics();
+           }
+        });
+    }
+
+    private void harvestStatistics() {
+        OffsetDateTime currentHarvest = OffsetDateTime.now();
+        for (List<CollectedStatistics> statistics : collectedStats.values()) {
+            Iterator<CollectedStatistics> stIterator = statistics.iterator();
+            while (stIterator.hasNext()) {
+                CollectedStatistics st = stIterator.next();
+                if (currentHarvest.isAfter(st.endTime())) {
+                    handle(st.statistics());
+                    stIterator.remove();
+                }
+            }
+        }
+        lastHarvesting = currentHarvest.toInstant();
+    }
+
+    private boolean overtakeHarvestTime() {
+        return lastHarvesting.plusSeconds(minToSec(MAX_WAITING_TO_HARVEST_MIN)).isBefore(Instant.now());
+    }
+
+    protected List<Threshold> thresholds() {
+        return thresholds;
+    }
+
+    protected Map<Long, Group> groups() {
+        return groupMap;
+    }
+
+    private void collect(Observation value) {
+        Group group = groupMap.get(value.source().groupId());
+
+        List<CollectedStatistics> groupStatistics = collectedStats.computeIfAbsent(group.id(), g -> new ArrayList<>());
+
+        OffsetDateTime timestamp = value.timestamp();
+        boolean newDataAccepted = false;
+        Iterator<CollectedStatistics> statisticsIterator = groupStatistics.iterator();
+        while(statisticsIterator.hasNext()) {
+            CollectedStatistics st = statisticsIterator.next();
+            if (timestamp.isEqual(st.startTime()) ||    // startInterval <= timestamp < endInterval
+                    (timestamp.isAfter(st.startTime()) && timestamp.isBefore(st.endTime()))
+            ) {
+                st.accept(value.source(), value.value());
+                newDataAccepted = true;
+            } else if (timestamp.isAfter(st.endTime())) {
+                handle(st.statistics());
+                statisticsIterator.remove();
+            }
+        }
+        lastHarvesting = Instant.now();
+
+        if (!newDataAccepted) {
+            CollectedStatistics newSt = CollectedStatistics.init(group, value.timestamp());
+            newSt.accept(value.source(), value.value());
+            groupStatistics.add(newSt);
+        }
+    }
+
+    @Override
+    public void run() {
+        eventBus().<Observation>consumer(id(), v -> collect(v.body()));
+    }
+
+    protected final void persist(StatisticRecord record) {
+        statisticsRepo.saveStatisticRecord(record).onFailure(logger::catching);
+    }
+}

+ 52 - 0
src/main/java/cz/senslog/analytics/module/api/Module.java

@@ -0,0 +1,52 @@
+package cz.senslog.analytics.module.api;
+
+import cz.senslog.analytics.domain.CollectorType;
+import cz.senslog.analytics.module.*;
+import cz.senslog.analytics.repository.*;
+import io.vertx.pgclient.PgPool;
+import io.vertx.sqlclient.Pool;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class Module {
+
+    private static final Map<Class<? extends SimpleModule>, SimpleModule> MODULES = new HashMap<>();
+
+    private static final Map<CollectorType, CollectorModule> COLLECTORS  = new HashMap<>();
+
+    public static SimpleModule[] createModules(Pool sensLogDB) {
+        ConfigurationRepository configRepo = new AnalyticsConfigRepository(sensLogDB);
+        SensLogDataRepository dataRepo = new SensLogDataRepository(sensLogDB);
+        AnalyticsDataRepository statisticsRep = new AnalyticsDataRepository(sensLogDB);
+
+        createModule(new ObservationReceiverModule(configRepo));
+        createModule(new DoubleStatisticsModule(configRepo, statisticsRep));
+        createModule(new MoldAnalysisModule(configRepo, statisticsRep));
+
+        createModule(new NotificationModule(configRepo));
+        createModule(new ScheduleDBLoaderModule(configRepo, dataRepo));
+
+        return MODULES.values().toArray(new SimpleModule[0]);
+    }
+
+    private static void createModule(SimpleModule module) {
+        if (module instanceof CollectorModule c) {
+            if (!COLLECTORS.containsKey(c.type())) {
+                COLLECTORS.put(c.type(), c);
+            }
+        }
+        if (!MODULES.containsKey(module.getClass())) {
+            MODULES.put(module.getClass(), module);
+        }
+    }
+
+    public static SimpleModule of(Class<? extends SimpleModule> aClass) {
+        return MODULES.get(aClass);
+    }
+
+    public static ModuleDescriptor collectorOf(CollectorType type) {
+        if (type == null) { return null; }
+        return COLLECTORS.get(type);
+    }
+}

+ 5 - 0
src/main/java/cz/senslog/analytics/module/api/ModuleDescriptor.java

@@ -0,0 +1,5 @@
+package cz.senslog.analytics.module.api;
+
+public interface ModuleDescriptor {
+    String id();
+}

+ 36 - 0
src/main/java/cz/senslog/analytics/module/api/SimpleModule.java

@@ -0,0 +1,36 @@
+package cz.senslog.analytics.module.api;
+
+import cz.senslog.analytics.domain.ThresholdViolationNotification;
+import cz.senslog.analytics.module.NotificationModule;
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.eventbus.EventBus;
+
+public abstract class SimpleModule extends AbstractVerticle implements ModuleDescriptor {
+
+    protected abstract void configure();
+
+    protected abstract void run() throws Exception;
+
+    public final void reconfigure() {
+        configure();
+    }
+
+    @Override
+    public final String id() {
+        return String.format("__module.%s", getClass().getSimpleName());
+    }
+
+    @Override
+    public final void start() throws Exception {
+        configure();
+        run();
+    }
+
+    protected final EventBus eventBus() {
+        return vertx.eventBus();
+    }
+
+    protected final void notify(ThresholdViolationNotification notification) {
+        eventBus().publish(Module.of(NotificationModule.class).id(), notification);
+    }
+}

+ 128 - 0
src/main/java/cz/senslog/analytics/repository/AnalyticsConfigRepository.java

@@ -0,0 +1,128 @@
+package cz.senslog.analytics.repository;
+
+import cz.senslog.analytics.domain.*;
+import cz.senslog.analytics.utils.Tuple;
+import cz.senslog.analytics.utils.validator.NotifyTrigger;
+import io.vertx.core.Future;
+import io.vertx.sqlclient.Pool;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+public final class AnalyticsConfigRepository implements ConfigurationRepository {
+
+    private final Pool client;
+
+    public AnalyticsConfigRepository(Pool client) {
+        this.client = client;
+    }
+
+
+    @Override
+    public Future<Map<Sensor, List<Group>>> loadSensorToGroupsMapping() {
+        return client.query("SELECT s.sensor_id, s.unit_id, sg.analytic_group_id, g.collector_type FROM analytics.entity_source_to_analytic_group sg " +
+                        "    JOIN analytics.analytic_group g on g.id = sg.id " +
+                        "    JOIN analytics.entity_source s on s.id = sg.entity_source_id")
+                .execute()
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(row -> Tuple.of(
+                                new Sensor(-1, // TODO is ID important?
+                                    row.getLong("unit_id"),
+                                    row.getLong("sensor_id"),
+                                    row.getLong("analytic_group_id")
+                                ), new Group(
+                                    row.getLong("analytic_group_id"),
+                                    CollectorType.valueOf(row.getString("collector_type"))
+                                ))
+                        ).collect(Collectors.groupingBy(
+                                Tuple::getItem1, HashMap::new, Collectors.mapping(Tuple::getItem2, Collectors.toList()))
+                        )
+                );
+    }
+
+    @Override
+    public Future<List<Threshold>> loadSensorThresholds() {
+        return client.query("SELECT th.id, th.analytic_group_id, th.notify_trigger_mode, th.attribute_type, th.process_on_fail, tr.threshold_mode, tr.threshold_value " +
+                        "FROM analytics.threshold AS th " +
+                        "JOIN analytics.analytic_group AS gi ON gi.id = th.analytic_group_id " +
+                        "JOIN analytics.threshold_rule tr on th.id = tr.threshold_id " +
+                        "WHERE gi.collector_type IS NULL")
+                .execute()
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(row -> new Threshold(
+                                        row.getLong("id"),
+                                        row.getLong("analytic_group_id"),
+                                        NotifyTrigger.Type.valueOf(row.getString("notify_trigger_mode")),
+                                        row.getBoolean("process_on_fail"),
+                                        AttributeName.valueOf(row.getString("attribute_type")),
+                                        new ArrayList<>() {{
+                                            add(new ThresholdRule(
+                                                    row.getString("threshold_mode"),
+                                                    row.getDouble("threshold_value")
+                                            ));
+                                        }}
+                                    )
+                        ).collect(Collectors.toMap(Threshold::id, t -> t, (p, q) -> {
+                            p.rules().add(q.rules().get(0)); return p;
+                        })).values().stream().toList()
+                );
+    }
+
+    @Override
+    public Future<List<MessageBrokerConfig>> loadMessageBrokers() {
+        // TODO waits to redesign the messaging
+        return Future.succeededFuture(Collections.emptyList());
+    }
+
+    @Override
+    public Future<List<SourceToMessageBroker>> loadSourceToMessageBrokerMapping() {
+        // TODO waits to redesign the messaging
+        return Future.succeededFuture(Collections.emptyList());
+    }
+
+    @Override
+    public Future<Map<Long, Group>> loadGroupsByCollectorType(CollectorType type) {
+        return client.preparedQuery("SELECT id, name, time_interval, persistence FROM analytics.analytic_group WHERE collector_type = $1")
+                .execute(io.vertx.sqlclient.Tuple.of(type.name()))
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map((row -> new Group(
+                                row.getLong("id"),
+                                row.getString("name"),
+                                row.getInteger("time_interval"),
+                                row.getBoolean("persistence"),
+                                type
+                        )))
+                        .collect(Collectors.toMap(Group::id, Function.identity()))
+        );
+    }
+
+    @Override
+    public Future<List<Threshold>> loadGroupThresholds(CollectorType type) {
+        return client.preparedQuery("SELECT th.id, th.analytic_group_id, th.notify_trigger_mode, th.attribute_type, th.process_on_fail, tr.threshold_mode, tr.threshold_value " +
+                        "FROM analytics.threshold AS th " +
+                        "JOIN analytics.analytic_group AS gi ON gi.id = th.analytic_group_id " +
+                        "JOIN analytics.threshold_rule tr on th.id = tr.threshold_id " +
+                        "WHERE gi.collector_type = $1")
+                .execute(io.vertx.sqlclient.Tuple.of(type.name()))
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(row -> new Threshold(
+                                        row.getLong("id"),
+                                        row.getLong("analytic_group_id"),
+                                        NotifyTrigger.Type.valueOf(row.getString("notify_trigger_mode")),
+                                        row.getBoolean("process_on_fail"),
+                                        AttributeName.valueOf(row.getString("attribute_type")),
+                                        new ArrayList<>() {{
+                                            add(new ThresholdRule(
+                                                    row.getString("threshold_mode"),
+                                                    row.getDouble("threshold_value")
+                                            ));
+                                        }}
+                                )
+                        ).collect(Collectors.toMap(Threshold::id, t -> t, (p, q) -> {
+                            p.rules().add(q.rules().get(0)); return p;
+                        })).values().stream().toList()
+                );
+    }
+}

+ 34 - 0
src/main/java/cz/senslog/analytics/repository/AnalyticsDataRepository.java

@@ -0,0 +1,34 @@
+package cz.senslog.analytics.repository;
+
+import cz.senslog.analytics.domain.StatisticRecord;
+import io.vertx.core.Future;
+import io.vertx.pgclient.PgPool;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.Tuple;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public final class AnalyticsDataRepository {
+
+    private static final Logger logger = LogManager.getLogger(AnalyticsDataRepository.class);
+
+    private final Pool client;
+
+    public AnalyticsDataRepository(Pool client) {
+        this.client = client;
+    }
+
+    public Future<Integer> saveStatisticRecord(StatisticRecord record) {
+        if (record == null) { return Future.failedFuture("StatisticRecord to save is null."); }
+        return client.preparedQuery("INSERT INTO analytics.records(group_id, value_attribute, record_value, time_interval, time_stamp) " +
+                        "VALUES ($1, $2, $3, $4, $5) RETURNING (id)")
+                .execute(Tuple.of(record.groupId(),
+                        record.valueAttribute(),
+                        record.recordValue(),
+                        record.timeInterval(),
+                        record.timestamp()
+                ))
+                .map(rs -> rs.iterator().next().getInteger("id"))
+                .onFailure(logger::error);
+    }
+}

+ 22 - 0
src/main/java/cz/senslog/analytics/repository/ConfigurationRepository.java

@@ -0,0 +1,22 @@
+package cz.senslog.analytics.repository;
+
+import cz.senslog.analytics.domain.*;
+import io.vertx.core.Future;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ConfigurationRepository {
+
+    Future<Map<Sensor, List<Group>>> loadSensorToGroupsMapping();
+
+    Future<List<Threshold>> loadSensorThresholds();
+
+    Future<List<MessageBrokerConfig>> loadMessageBrokers();
+
+    Future<List<SourceToMessageBroker>> loadSourceToMessageBrokerMapping();
+
+    Future<Map<Long, Group>> loadGroupsByCollectorType(CollectorType type);
+
+    Future<List<Threshold>> loadGroupThresholds(CollectorType type);
+}

+ 41 - 0
src/main/java/cz/senslog/analytics/repository/MockConfigRepository.java

@@ -0,0 +1,41 @@
+package cz.senslog.analytics.repository;
+
+import cz.senslog.analytics.MockData;
+import cz.senslog.analytics.domain.*;
+import io.vertx.core.Future;
+
+import java.util.List;
+import java.util.Map;
+
+public class MockConfigRepository implements ConfigurationRepository {
+
+    @Override
+    public Future<Map<Sensor, List<Group>>> loadSensorToGroupsMapping() {
+        return Future.succeededFuture(MockData.mockSensorToGroupConfig());
+    }
+
+    @Override
+    public Future<List<Threshold>> loadSensorThresholds() {
+        return Future.succeededFuture(MockData.mockThresholdsConfigForSensors());
+    }
+
+    @Override
+    public Future<List<MessageBrokerConfig>> loadMessageBrokers() {
+        return Future.succeededFuture(MockData.mockMessageBrokers());
+    }
+
+    @Override
+    public Future<List<SourceToMessageBroker>> loadSourceToMessageBrokerMapping() {
+        return Future.succeededFuture(MockData.mockSourceToMessageBroker());
+    }
+
+    @Override
+    public Future<Map<Long, Group>> loadGroupsByCollectorType(CollectorType type) {
+        return Future.succeededFuture(MockData.mockGroupsConfig(type));
+    }
+
+    @Override
+    public Future<List<Threshold>> loadGroupThresholds(CollectorType type) {
+        return Future.succeededFuture(MockData.mockThresholdsConfigForGroups(type));
+    }
+}

+ 77 - 0
src/main/java/cz/senslog/analytics/repository/SensLogDataRepository.java

@@ -0,0 +1,77 @@
+package cz.senslog.analytics.repository;
+
+import cz.senslog.analytics.domain.Observation;
+import cz.senslog.analytics.domain.Sensor;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.pgclient.PgPool;
+import io.vertx.sqlclient.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+public final class SensLogDataRepository {
+
+    private static final Logger logger = LogManager.getLogger(SensLogDataRepository.class);
+
+    private final Pool client;
+
+    public SensLogDataRepository(Pool client) {
+        this.client = client;
+    }
+
+    public Future<List<Observation>> loadObservations(OffsetDateTime from, OffsetDateTime to) {
+        return client.query("SELECT unit_id, sensor_id, observed_value, time_stamp " +
+                        "FROM export.observations ORDER BY time_stamp LIMIT 1000")
+                .execute()
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(row -> new Observation(
+                                new Sensor(
+                                        row.getLong("unit_id"),
+                                        row.getLong("sensor_id")
+                                ),
+                                row.getDouble("observed_value"),
+                                row.getOffsetDateTime("time_stamp"))
+                        ).collect(Collectors.toList())
+                );
+    }
+
+    public void cursorAllObservations(int fetch, Handler<Observation> dataStreamHandler, Handler<Throwable> exceptionHandler) {
+        client.getConnection().onSuccess(conn -> conn
+                .prepare("SELECT observation_id, unit_id, sensor_id, observed_value, time_stamp " +
+                        "FROM export.observations ORDER BY time_stamp")
+                .onComplete(ar -> {
+                    if (ar.succeeded()) {
+                        PreparedStatement pq = ar.result();
+                        conn.begin().onComplete(ar1 -> {
+                            if (ar1.succeeded()) {
+                                Transaction tx = ar1.result();
+
+                                RowStream<Row> stream = pq.createStream(fetch);
+
+                                stream.exceptionHandler(exceptionHandler);
+                                stream.endHandler(v -> stream.close()
+                                        .onComplete(closed -> tx.commit()
+                                            .onComplete(committed ->
+                                                    logger.info("End of stream")
+                                            )
+                                        )
+                                );
+                                stream.handler(row -> dataStreamHandler.handle(new Observation(
+                                        new Sensor(
+                                                row.getLong("unit_id"),
+                                                row.getLong("sensor_id")
+                                        ),
+                                        row.getDouble("observed_value"),
+                                        row.getOffsetDateTime("time_stamp"))
+                                ));
+                            }
+                        });
+                    }
+                }));
+    }
+}

+ 8 - 22
src/main/java/cz/senslog/analyzer/util/DateTrunc.java → src/main/java/cz/senslog/analytics/utils/DateTrunc.java

@@ -1,4 +1,4 @@
-package cz.senslog.analyzer.util;
+package cz.senslog.analytics.utils;
 
 import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
@@ -54,27 +54,13 @@ public final class DateTrunc {
             int value = optionComponents[option.ordinal()];
             if (value == -1) { continue; }
             switch (option) {
-                case YEAR: {
-                    result = result.withYear(value == 0 ? 0 : result.getYear() - (result.getYear() % value));
-                } break;
-                case MONTH: {
-                    result = result.withMonth(value == 0 ? 1 : result.getMonthValue() - (result.getMonthValue() % value));
-                } break;
-                case WEEK: {
-                    // TODO: implement
-                } break;
-                case DAY: {
-                    result = result.withDayOfMonth(value == 0 ? 1 : result.getDayOfMonth() - (result.getDayOfMonth() % value));
-                } break;
-                case HOUR: {
-                    result = result.withHour(value == 0 ? 0 : result.getHour() - (result.getHour() % value));
-                } break;
-                case MINUTE: {
-                    result = result.withMinute(value == 0 ? 0 : result.getMinute() - (result.getMinute() % value));
-                } break;
-                case SECOND: {
-                    result = result.withSecond(value == 0 ? 0 : result.getSecond() - (result.getSecond() % value));
-                } break;
+                case YEAR   ->  result = result.withYear(value == 0 ? 0 : result.getYear() - (result.getYear() % value));
+                case MONTH  ->  result = result.withMonth(value == 0 ? 1 : result.getMonthValue() - (result.getMonthValue() % value));
+                case WEEK   ->  { /* TODO: implement */}
+                case DAY    ->  result = result.withDayOfMonth(value == 0 ? 1 : result.getDayOfMonth() - (result.getDayOfMonth() % value));
+                case HOUR   ->  result = result.withHour(value == 0 ? 0 : result.getHour() - (result.getHour() % value));
+                case MINUTE ->  result = result.withMinute(value == 0 ? 0 : result.getMinute() - (result.getMinute() % value));
+                case SECOND ->  result = result.withSecond(value == 0 ? 0 : result.getSecond() - (result.getSecond() % value));
             }
         }
         return result;

+ 12 - 0
src/main/java/cz/senslog/analytics/utils/TimeUtils.java

@@ -0,0 +1,12 @@
+package cz.senslog.analytics.utils;
+
+public final class TimeUtils {
+
+    public static long minToSec(int minute) {
+        return minute * 60L;
+    }
+
+    public static long minToMillis(int minute) {
+        return minute * 60L * 1000L;
+    }
+}

+ 39 - 0
src/main/java/cz/senslog/analytics/utils/Tuple.java

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

+ 22 - 0
src/main/java/cz/senslog/analytics/utils/validator/DisableNotifyTrigger.java

@@ -0,0 +1,22 @@
+package cz.senslog.analytics.utils.validator;
+
+
+import cz.senslog.analytics.domain.ValidationResult;
+
+public class DisableNotifyTrigger implements NotifyTrigger {
+
+    public DisableNotifyTrigger() {}
+
+    @Override
+    public void accept(ValidationResult validationResult) {}
+
+    @Override
+    public boolean shouldNotify() {
+        return false;
+    }
+
+    @Override
+    public ValidationResult[] resultsToNotify() {
+        return new ValidationResult[0];
+    }
+}

+ 48 - 0
src/main/java/cz/senslog/analytics/utils/validator/InstantNotifyTrigger.java

@@ -0,0 +1,48 @@
+package cz.senslog.analytics.utils.validator;
+
+import cz.senslog.analytics.domain.AttributeName;
+import cz.senslog.analytics.domain.ValidationResult;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class InstantNotifyTrigger implements NotifyTrigger {
+
+    private final Map<AttributeName, ValidationResult> currentResultMap;
+    private List<AttributeName> tempAttrsToNotify;
+
+    public InstantNotifyTrigger() {
+        this.currentResultMap = new HashMap<>();
+    }
+
+    @Override
+    public void accept(ValidationResult validationResult) {
+        currentResultMap.put(validationResult.attribute(), validationResult);
+    }
+
+    @Override
+    public boolean shouldNotify() {
+        boolean notify = false;
+        tempAttrsToNotify = new ArrayList<>();
+        for (ValidationResult res : currentResultMap.values()) {
+            if (res.isNotValid()) {
+                notify = true;
+                tempAttrsToNotify.add(res.attribute());
+            }
+        }
+        return notify;
+    }
+
+    @Override
+    public ValidationResult[] resultsToNotify() {
+        ValidationResult[] res = new ValidationResult[tempAttrsToNotify.size()];
+        int ind = 0;
+        for (AttributeName attr : tempAttrsToNotify) {
+            res[ind++] = currentResultMap.get(attr);
+        }
+        tempAttrsToNotify = null;
+        return res;
+    }
+}

+ 34 - 0
src/main/java/cz/senslog/analytics/utils/validator/NotifyTrigger.java

@@ -0,0 +1,34 @@
+package cz.senslog.analytics.utils.validator;
+
+
+import cz.senslog.analytics.domain.ValidationResult;
+
+import java.util.function.Supplier;
+
+public interface NotifyTrigger {
+
+    void accept(ValidationResult validationResult);
+
+    boolean shouldNotify();
+    ValidationResult[] resultsToNotify();
+
+    enum Type {
+        DISABLED    (DisableNotifyTrigger::new),
+        INSTANT     (InstantNotifyTrigger::new),
+        ON_CHANGE   (OnChangeNotifyTrigger::new),
+
+        ;
+
+        Type(Supplier<NotifyTrigger> constructCreator) {
+            this.constructCreator = constructCreator;
+        }
+
+        private final Supplier<NotifyTrigger> constructCreator;
+
+        public NotifyTrigger createInstance() {
+            return constructCreator.get();
+        }
+    }
+
+
+}

+ 80 - 0
src/main/java/cz/senslog/analytics/utils/validator/OnChangeNotifyTrigger.java

@@ -0,0 +1,80 @@
+package cz.senslog.analytics.utils.validator;
+
+
+import cz.senslog.analytics.domain.AttributeName;
+import cz.senslog.analytics.domain.ValidationResult;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OnChangeNotifyTrigger implements NotifyTrigger {
+
+    private static final class CacheWrapper {
+        private final ValidationResult[] resultsArr;
+        private int freeIndex;
+
+        private CacheWrapper(ValidationResult[] resultsArr, int freeIndex) {
+            this.resultsArr = resultsArr;
+            this.freeIndex = freeIndex;
+        }
+    }
+
+    private static final int CACHE_LEN = 2;
+
+    private final Map<AttributeName, CacheWrapper> cacheMap;
+
+    private List<ValidationResult> tempToNotify;
+
+    public OnChangeNotifyTrigger() {
+        this.cacheMap = new HashMap<>();
+    }
+
+    @Override
+    public void accept(ValidationResult validationResult) {
+        AttributeName attr = validationResult.attribute();
+        if (!cacheMap.containsKey(attr)) {
+            cacheMap.put(attr, new CacheWrapper(new ValidationResult[CACHE_LEN], 0));
+        }
+        CacheWrapper cache = cacheMap.get(attr);
+        cache.resultsArr[cache.freeIndex] = validationResult;
+        cache.freeIndex = nextIndex(cache.freeIndex);
+    }
+
+    private static int nextIndex(int index) {
+        return  (index + 1) % CACHE_LEN;
+    }
+
+    private static int previousIndex(int index) {
+        return (index <= 0 ? CACHE_LEN : index) - 1;
+    }
+
+    @Override
+    public boolean shouldNotify() {
+        boolean notify = false;
+        tempToNotify = new ArrayList<>(cacheMap.size());
+        for (CacheWrapper cache : cacheMap.values()) {
+            int currentDataInd = previousIndex(cache.freeIndex);
+            ValidationResult currentRes = cache.resultsArr[currentDataInd];
+            ValidationResult previousRes = cache.resultsArr[previousIndex(currentDataInd)];
+            if ((previousRes == null || previousRes.isValid()) && currentRes.isNotValid()) {
+                notify = true; // pass the threshold
+                tempToNotify.add(currentRes);
+            } else if(currentRes.isValid() && (previousRes != null && previousRes.isNotValid())) {
+                notify = true; // leave the threshold
+                tempToNotify.add(currentRes);
+            }
+        }
+        return notify;
+    }
+
+    @Override
+    public ValidationResult[] resultsToNotify() {
+        if (tempToNotify == null) { return new ValidationResult[0]; }
+        ValidationResult[] res = tempToNotify.toArray(new ValidationResult[0]);
+        tempToNotify = null;
+        return res;
+    }
+
+}

+ 81 - 0
src/main/java/cz/senslog/analytics/utils/validator/ThresholdChecker.java

@@ -0,0 +1,81 @@
+package cz.senslog.analytics.utils.validator;
+
+import cz.senslog.analytics.domain.*;
+
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import static java.util.Collections.emptyList;
+
+public class ThresholdChecker<DS extends TimeSeriesDatasource> {
+
+    public static <T extends TimeSeriesDatasource> ThresholdChecker<T> disabled() {
+        return ThresholdChecker.create(emptyList(), null, null);
+    }
+
+    private final Map<Long, List<Threshold>> thresholds;
+
+    private final Validator<DS> validator;
+
+    private final Consumer<ViolationReport> ifViolated;
+
+    private final Map<Long, NotifyTrigger> notifyTriggerMap;
+
+    public static <DS extends TimeSeriesDatasource> ThresholdChecker<DS> create(List<Threshold> thresholds,
+                                                           Validator<DS> validator,
+                                                           Consumer<ViolationReport> ifViolated)
+    {
+        return new ThresholdChecker<>(thresholds, validator, ifViolated);
+    }
+
+    public ThresholdChecker(List<Threshold> thresholds,
+                            Validator<DS> validator,
+                            Consumer<ViolationReport> ifViolated
+    ) {
+        this.validator = validator;
+        this.ifViolated = ifViolated;
+
+        this.notifyTriggerMap = new HashMap<>();
+        this.thresholds = new HashMap<>();
+        for (Threshold th : thresholds) {
+            long sourceId = th.groupId();
+            if (!notifyTriggerMap.containsKey(sourceId)) {
+                this.notifyTriggerMap.put(sourceId, th.notifyTriggerType().createInstance());
+            }
+            this.thresholds.computeIfAbsent(th.groupId(), k -> new ArrayList<>()).add(th);
+        }
+    }
+
+    public Predicate<DS> check() {
+        return this::validateThreshold;
+    }
+
+    private boolean validateThreshold(DS data) {
+        long sourceId = data.datasourceId();
+        OffsetDateTime timestamp = data.timestamp();
+
+        if (!thresholds.containsKey(sourceId)) {
+            return true;
+        }
+        boolean process = true;
+        List<Threshold> rules = thresholds.get(sourceId);
+
+        NotifyTrigger notifier = notifyTriggerMap.get(sourceId);
+        for (Threshold th : rules) {
+            ValidationResult res = validator.validate(data, th);
+            notifier.accept(res);
+
+            if (res.isNotValid() && !th.allowProcess()) {
+                process = false;
+            }
+        }
+
+        if (notifier.shouldNotify()) {
+            ifViolated.accept(new ViolationReport(sourceId, timestamp, notifier.resultsToNotify()));
+        }
+
+        return process;
+    }
+}

+ 79 - 0
src/main/java/cz/senslog/analytics/utils/validator/Validator.java

@@ -0,0 +1,79 @@
+package cz.senslog.analytics.utils.validator;
+
+import cz.senslog.analytics.domain.AttributeName;
+import cz.senslog.analytics.domain.Threshold;
+import cz.senslog.analytics.domain.ThresholdRule;
+import cz.senslog.analytics.domain.ValidationResult;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class Validator<T> {
+
+    private static final Map<String, BiFunction<Double, Double, Boolean>> functions;
+
+    static {
+        functions = new HashMap<>(6);
+        functions.put("gt", (val, ths) -> val > ths);
+        functions.put("ge", (val, ths) -> val >= ths);
+        functions.put("lt", (val, ths) -> val < ths);
+        functions.put("le", (val, ths) -> val <= ths);
+        functions.put("eq", Double::equals);
+        functions.put("ne", (val, ths) -> !val.equals(ths));
+    }
+
+    public static boolean checkThresholdValue(String mode, Double value, Double threshold) {
+        if (mode == null || value == null || threshold == null) return false;
+        return functions.getOrDefault(mode, (val, ths) -> false).apply(value, threshold);
+    }
+
+    public static boolean checkThresholdValue(ThresholdRule rule, Double value) {
+        if (rule == null || value == null) return false;
+        return checkThresholdValue(rule.mode(), value, rule.value());
+    }
+
+    public interface AttributeMapping<T> {
+        Validator<T> addMapping(AttributeName property, Function<T, Supplier<Double>> getter);
+    }
+
+
+    public static <T> AttributeMapping<T> create() {
+        return new AttributeMapping<>() {
+            private final Validator<T> validator = new Validator<>();
+            @Override
+            public Validator<T> addMapping(AttributeName property, Function<T, Supplier<Double>> getter) {
+                validator.map.put(property, getter);
+                return validator;
+            }
+        };
+    }
+
+    private final Map<AttributeName, Function<T, Supplier<Double>>> map;
+
+    private Validator() {
+        this.map = new HashMap<>();
+    }
+
+    public Validator<T> addMapping(AttributeName property, Function<T, Supplier<Double>> getter) {
+        map.put(property, getter);
+        return this;
+    }
+
+    public ValidationResult validate(T object, Threshold threshold) {
+        return validate(object, threshold, map);
+    }
+
+    public static <T> ValidationResult validate(T object, Threshold threshold, Map<AttributeName, Function<T, Supplier<Double>>> attributeMapping) {
+        Double value = attributeMapping.getOrDefault(threshold.attribute(), (o) -> () -> null).apply(object).get();
+        ValidationResult result = new ValidationResult(threshold.attribute(), value);
+        for (ThresholdRule rule : threshold.rules()) {
+            if (checkThresholdValue(rule, value)) {
+                result.addRecord(rule.mode(), rule.value());
+            }
+        }
+        return result;
+    }
+}

+ 0 - 24
src/main/java/cz/senslog/analyzer/analysis/AggregationUnit.java

@@ -1,24 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-public enum AggregationUnit {
-
-    HOUR        (60),
-    HALF_DAY    (720),
-    DAY         (1440),
-    WEAK        (10080),
-
-    ;
-
-    private final long minutes;
-    AggregationUnit(int minutes) {
-        this.minutes = minutes;
-    }
-
-    public long getMinutes() {
-        return minutes;
-    }
-
-    public long getSeconds() {
-        return getMinutes() * 60;
-    }
-}

+ 0 - 12
src/main/java/cz/senslog/analyzer/analysis/Analyzer.java

@@ -1,12 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import java.util.List;
-
-public interface Analyzer<T> {
-
-    void accept(List<T> observations);
-
-    AnalyzerInfo info();
-
-    void reloadConfig();
-}

+ 0 - 15
src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java

@@ -1,15 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import cz.senslog.analyzer.domain.Observation;
-import dagger.Component;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-@Singleton
-@Component(modules = AnalyzerModule.class)
-public interface AnalyzerComponent {
-
-    @Named("statisticsAnalyzer")
-    Analyzer<Observation> createObservationAnalyzer();
-}

+ 0 - 4
src/main/java/cz/senslog/analyzer/analysis/AnalyzerConfigInfo.java

@@ -1,4 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-public class AnalyzerConfigInfo {
-}

+ 0 - 88
src/main/java/cz/senslog/analyzer/analysis/AnalyzerInfo.java

@@ -1,88 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.util.Collections.unmodifiableList;
-
-public class AnalyzerInfo {
-
-    enum Status {
-
-    }
-
-    static class HandlerInfo {
-
-        private final String name;
-
-        private final Status status;
-
-        private final String configHash;
-
-        public HandlerInfo(String name, Status status, String configHash) {
-            this.name = name;
-            this.status = status;
-            this.configHash = configHash;
-        }
-
-        public String getName() {
-            return name;
-        }
-
-        public Status getStatus() {
-            return status;
-        }
-
-        public String getConfigHash() {
-            return configHash;
-        }
-    }
-
-
-    public static Builder create(Status status) {
-        return new BuilderImpl(status);
-    }
-
-    interface Builder {
-        Builder addHandlerInfo(HandlerInfo handlerInfo);
-        AnalyzerInfo get();
-    }
-
-    private static class BuilderImpl implements Builder {
-
-        private List<HandlerInfo> handlers;
-        private Status status;
-
-        private BuilderImpl(Status status) {
-            this.status = status;
-            this.handlers = new ArrayList<>();
-        }
-
-        @Override
-        public Builder addHandlerInfo(HandlerInfo handlerInfo) {
-            handlers.add(handlerInfo);
-            return this;
-        }
-
-        @Override
-        public AnalyzerInfo get() {
-            return new AnalyzerInfo(unmodifiableList(handlers), status);
-        }
-    }
-
-    private final List<HandlerInfo> handlerInfos;
-    private final Status status;
-
-    private AnalyzerInfo(List<HandlerInfo> handlerInfos, Status status) {
-        this.handlerInfos = handlerInfos;
-        this.status = status;
-    }
-
-    public List<HandlerInfo> getHandlerInfos() {
-        return handlerInfos;
-    }
-
-    public Status getStatus() {
-        return status;
-    }
-}

+ 0 - 60
src/main/java/cz/senslog/analyzer/analysis/AnalyzerModule.java

@@ -1,60 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import cz.senslog.analyzer.analysis.module.FilterHandler;
-import cz.senslog.analyzer.analysis.module.HandlersModule;
-import cz.senslog.analyzer.analysis.module.ThresholdHandler;
-import cz.senslog.analyzer.core.EventBusModule;
-import cz.senslog.analyzer.core.api.BlockingHandler;
-import cz.senslog.analyzer.core.EventBus;
-import cz.senslog.analyzer.core.api.HandlerInvoker;
-import cz.senslog.analyzer.domain.DoubleStatistics;
-import cz.senslog.analyzer.domain.Observation;
-
-import dagger.Module;
-import dagger.Provides;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import javax.inject.Named;
-
-import static cz.senslog.analyzer.core.api.HandlerInvoker.cancelInvoker;
-
-
-@Module(includes = {
-        HandlersModule.class,
-        EventBusModule.class
-})
-public class AnalyzerModule {
-
-    private static final Logger logger = LogManager.getLogger(AnalyzerModule.class);
-
-    @Provides @Named("statisticsAnalyzer")
-    Analyzer<Observation> provideSimpleAnalyzer (
-            @Named("sensorFilterHandler") FilterHandler<Observation> sensorFilterHandler,
-            @Named("sensorThresholdHandler") ThresholdHandler<Observation> sensorThresholdHandler,
-            @Named("aggregationCollectorHandler") BlockingHandler<Observation, DoubleStatistics> aggregateCollectorHandler,
-            @Named("groupThresholdHandler") ThresholdHandler<DoubleStatistics> groupThresholdHandler,
-            EventBus eventBus
-    ) {
-        // filter & group mapping ===>> check sensor value ====>> aggregate by groups ====>> check group statistics
-
-        HandlerInvoker<DoubleStatistics> groupThreshold = HandlerInvoker.create()
-                .handler(groupThresholdHandler).nextHandlerInvoker(cancelInvoker())
-                .eventBus(eventBus).build();
-
-        HandlerInvoker<Observation> aggregateCollector = HandlerInvoker.create()
-                .blockingHandler(aggregateCollectorHandler, eventBus::save)
-                .nextHandlerInvoker(groupThreshold)
-                .eventBus(eventBus).build();
-
-        HandlerInvoker<Observation> sensorThreshold = HandlerInvoker.create()
-                .handler(sensorThresholdHandler).nextHandlerInvoker(aggregateCollector)
-                .eventBus(eventBus).build();
-
-        HandlerInvoker<Observation> sensorFilter = HandlerInvoker.create()
-                .handler(sensorFilterHandler).nextHandlerInvoker(sensorThreshold)
-                .eventBus(eventBus).build();
-
-        return new ObservationAnalyzer(sensorFilter);
-    }
-}

+ 0 - 33
src/main/java/cz/senslog/analyzer/analysis/Checker.java

@@ -1,33 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import cz.senslog.analyzer.domain.Threshold;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.BiFunction;
-
-public class Checker {
-
-    private static final Map<String, BiFunction<Double, Double, Boolean>> functions;
-
-    static {
-        functions = new HashMap<>(6);
-        functions.put("gt", (val, ths) -> val > ths);
-        functions.put("ge", (val, ths) -> val >= ths);
-        functions.put("lt", (val, ths) -> val < ths);
-        functions.put("le", (val, ths) -> val <= ths);
-        functions.put("eq", (val, ths) -> val.equals(ths));
-        functions.put("ne", (val, ths) -> !val.equals(ths));
-    }
-
-    public static boolean checkThresholdValue(String mode, Double value, Double threshold) {
-        if (mode == null || value == null || threshold == null) return false;
-        return functions.getOrDefault(mode, (val, ths) -> false).apply(value, threshold);
-    }
-
-    public static boolean checkThresholdValue(Threshold.Rule rule, Double value) {
-        if (rule == null || value == null) return false;
-        return checkThresholdValue(rule.getMode(), value, rule.getValue());
-    }
-
-}

+ 0 - 16
src/main/java/cz/senslog/analyzer/analysis/ManualAnalyticsConfig.java

@@ -1,16 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import java.util.List;
-
-public class ManualAnalyticsConfig {
-
-    private final List<TaskConfig> tasks;
-
-    public ManualAnalyticsConfig(List<TaskConfig> tasks) {
-        this.tasks = tasks;
-    }
-
-    public List<TaskConfig> getTasks() {
-        return tasks;
-    }
-}

+ 0 - 38
src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java

@@ -1,38 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import cz.senslog.analyzer.core.api.HandlerInvoker;
-import cz.senslog.analyzer.domain.Observation;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.List;
-
-public class ObservationAnalyzer implements Analyzer<Observation> {
-
-    private static final Logger logger = LogManager.getLogger(ObservationAnalyzer.class);
-
-    private final HandlerInvoker<Observation> invoker;
-
-    public ObservationAnalyzer(HandlerInvoker<Observation> invoker) {
-        this.invoker = invoker;
-    }
-
-    @Override
-    public void accept(List<Observation> observations) {
-        synchronized (this) {
-            logger.info("Processing of the new {} observations.", observations.size());
-            invoker.accept(observations);
-            logger.info("The process of processing new observations is finished.");
-        }
-    }
-
-    @Override
-    public AnalyzerInfo info() {
-        return null;
-    }
-
-    @Override
-    public void reloadConfig() {
-        // TODO reload all modules' settings
-    }
-}

+ 0 - 69
src/main/java/cz/senslog/analyzer/analysis/TaskConfig.java

@@ -1,69 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-import java.time.OffsetDateTime;
-import java.util.Objects;
-import java.util.Set;
-
-public class TaskConfig {
-    private final String id;
-    private final OffsetDateTime from, to;
-    private final Set<Long> groupIds;
-
-    public TaskConfig(String id, Set<Long> groupIds, OffsetDateTime from, OffsetDateTime to) {
-        Objects.requireNonNull(id);
-        Objects.requireNonNull(groupIds);
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(to);
-
-        OffsetDateTime now = OffsetDateTime.now();
-        if (from.isAfter(now)) {
-            throw new UnsupportedOperationException(String.format(
-                    "Value 'from' can not be in future for the task '%s'.", id
-            ));
-        }
-        if (to.isAfter(now)) {
-            throw new UnsupportedOperationException(String.format(
-                    "Value 'to' can not be in future for the task '%s'.", id
-            ));
-        }
-        if (to.isBefore(from) || to.isEqual(from)) {
-            throw new UnsupportedOperationException(String.format(
-                    "Value 'from' and 'to' does not create an interval in the task '%s'.", id
-            ));
-        }
-
-        this.id = id;
-        this.from = from;
-        this.to = to;
-        this.groupIds = groupIds;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public OffsetDateTime getFrom() {
-        return from;
-    }
-
-    public OffsetDateTime getTo() {
-        return to;
-    }
-
-    public Set<Long> getGroupIds() {
-        return groupIds;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        TaskConfig that = (TaskConfig) o;
-        return id.equals(that.id);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(id);
-    }
-}

+ 0 - 115
src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java

@@ -1,115 +0,0 @@
-package cz.senslog.analyzer.analysis.module;
-
-import cz.senslog.analyzer.core.api.BlockingHandler;
-import cz.senslog.analyzer.core.api.DataFinisher;
-import cz.senslog.analyzer.core.api.HandlerContext;
-import cz.senslog.analyzer.domain.*;
-import cz.senslog.analyzer.storage.inmemory.CollectedStatisticsStorage;
-import cz.senslog.analyzer.storage.inmemory.TimestampStorage;
-import cz.senslog.analyzer.util.DateTrunc;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.*;
-import java.util.function.Function;
-
-import static cz.senslog.analyzer.domain.TimestampType.LAST_COMMITTED_INCLUSIVE;
-
-
-public abstract class CollectorHandler<I extends Data<?, ?>> extends BlockingHandler<I, DoubleStatistics> {
-
-    private static final Logger logger = LogManager.getLogger(CollectorHandler.class);
-
-    /** Map of saved groups (Map<group_id, Group>). */
-    private Map<Long, Group> groupsGroupById;
-
-    private Map<Group, List<CollectedStatistics>> collectedStatistics;
-
-    private final CollectedStatisticsStorage statisticsStorage;
-    private final TimestampStorage timestampStorage;
-
-    public CollectorHandler(CollectedStatisticsStorage statisticsStorage, TimestampStorage timestampStorage) {
-        this.statisticsStorage = statisticsStorage;
-        this.timestampStorage = timestampStorage;
-    }
-
-    protected abstract List<Group> loadGroups();
-    protected abstract Function<I, Boolean> collectData(DoubleStatistics statistics);
-    protected abstract long getGroupId(I data);
-
-    @Override
-    public void init() {
-        List<Group> groups = loadGroups();
-        groupsGroupById = new HashMap<>(groups.size());
-        collectedStatistics = new HashMap<>(groups.size());
-        for (Group group : groups) {
-            groupsGroupById.put(group.getId(), group);
-            collectedStatistics.put(group, statisticsStorage.restore(group));
-        }
-    }
-
-    @Override
-    public void finish(DataFinisher<DoubleStatistics> finisher, Timestamp edgeDateTime) {
-        logger.info("Finishing collecting data at the time {}.", edgeDateTime);
-        List<DoubleStatistics> finishedData = new ArrayList<>();
-        for (Group group : groupsGroupById.values()) {
-            List<CollectedStatistics> statistics = getCollectedStatisticsByGroup(group);
-            Iterator<CollectedStatistics> statisticsIterator = statistics.iterator();
-            while (statisticsIterator.hasNext()) {
-                CollectedStatistics st = statisticsIterator.next();
-                if (st.getEndTime().isBefore(edgeDateTime)) {
-                    finishedData.add(st.getStatistics());
-                    statisticsIterator.remove();
-                    statisticsStorage.remove(st);
-                }
-            }
-        }
-        String providerName = Thread.currentThread().getName();
-        if (statisticsStorage.commit() && providerName.contains("db-scheduler")) { // TODO temporary hack
-            timestampStorage.update(edgeDateTime, LAST_COMMITTED_INCLUSIVE);
-        }
-        finisher.finish(finishedData);
-    }
-
-    @Override
-    public void handle(HandlerContext<I, DoubleStatistics> context) {
-        I data = context.data();
-        long groupId = getGroupId(context.data());
-        Timestamp timestamp = data.getTimestamp();
-        Group group = getGroupByGroupId(groupId);
-        logger.trace("Handling data for group: {} at {}.", groupId, timestamp);
-
-        if (group.getInterval() <= 0) { return; }
-
-        List<CollectedStatistics> groupStatistics = getCollectedStatisticsByGroup(group);
-
-        boolean newDataAccepted = false;
-        for (CollectedStatistics st : groupStatistics) { // startInterval <= timestamp < endInterval
-            if (timestamp.isEqual(st.getStartTime()) ||
-                    (timestamp.isAfter(st.getStartTime()) && timestamp.isBefore(st.getEndTime()))
-            ) {
-                collectData(st.getStatistics()).apply(data);
-                newDataAccepted = true;
-            }
-        }
-
-        if (!newDataAccepted) { // register a new statistics
-            Timestamp startOfInterval = createStartOfInterval(timestamp, group);
-            CollectedStatistics newSt = new CollectedStatistics(group, startOfInterval);
-            collectData(newSt.getStatistics()).apply(data);
-            groupStatistics.add(statisticsStorage.watch(newSt));
-        }
-    }
-
-    private static Timestamp createStartOfInterval(Timestamp timestamp, Group group) {
-        return Timestamp.of(DateTrunc.trunc(timestamp.get(), (int)group.getInterval()));
-    }
-
-    private Group getGroupByGroupId(long groupId) {
-        return groupsGroupById.getOrDefault(groupId, Group.empty());
-    }
-
-    private List<CollectedStatistics> getCollectedStatisticsByGroup(Group group) {
-        return collectedStatistics.computeIfAbsent(group, g -> new ArrayList<>());
-    }
-}

+ 0 - 48
src/main/java/cz/senslog/analyzer/analysis/module/FilterHandler.java

@@ -1,48 +0,0 @@
-package cz.senslog.analyzer.analysis.module;
-
-import cz.senslog.analyzer.core.api.Handler;
-import cz.senslog.analyzer.core.api.HandlerContext;
-import cz.senslog.analyzer.domain.Data;
-import cz.senslog.analyzer.domain.Sensor;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.*;
-
-import static java.util.Collections.emptySet;
-
-public abstract class FilterHandler<T extends Data<?, ?>> extends Handler<T, T> {
-
-    private static final Logger logger = LogManager.getLogger(FilterHandler.class);
-
-    /** Map of sensors and theirs groups where they are assigned (Map<Sensor, Set<group_is>>). */
-    private Map<Sensor, Set<Long>> sensorMapping;
-
-    protected abstract List<Sensor> loadMapping();
-    protected abstract T newData(Sensor sensor, T data);
-    protected abstract Sensor getSensor(T data);
-
-    @Override
-    public void init() {
-        List<Sensor> sensors = loadMapping();
-        sensorMapping = new HashMap<>(sensors.size());
-        for (Sensor sensor : sensors) {
-            sensorMapping.computeIfAbsent(sensor, k -> new HashSet<>())
-                    .add(sensor.getGroupId());
-        }
-    }
-
-    @Override
-    public void handle(HandlerContext<T, T> context) {
-        Sensor sensor = getSensor(context.data());
-        Set<Long> sensorGroupsIds = getGroupIdsBySensor(sensor);
-
-        for (long groupId : sensorGroupsIds) {
-            context.next(newData(new Sensor(sensor, groupId), context.data()));
-        }
-    }
-
-    private Set<Long> getGroupIdsBySensor(Sensor sensor) {
-        return sensorMapping.getOrDefault(sensor, emptySet());
-    }
-}

+ 0 - 79
src/main/java/cz/senslog/analyzer/analysis/module/HandlersModule.java

@@ -1,79 +0,0 @@
-package cz.senslog.analyzer.analysis.module;
-
-import cz.senslog.analyzer.core.api.BlockingHandler;
-import cz.senslog.analyzer.domain.*;
-import cz.senslog.analyzer.storage.RepositoryModule;
-import cz.senslog.analyzer.storage.inmemory.CollectedStatisticsStorage;
-import cz.senslog.analyzer.storage.inmemory.TimestampStorage;
-import cz.senslog.analyzer.storage.inmemory.repository.CollectedStatisticsRepository;
-import cz.senslog.analyzer.storage.inmemory.repository.TimestampRepository;
-import cz.senslog.analyzer.storage.permanent.repository.StatisticsConfigRepository;
-import dagger.Module;
-import dagger.Provides;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-import java.util.*;
-import java.util.function.Function;
-
-@Module(includes = RepositoryModule.class)
-public class HandlersModule {
-
-    private static final Logger logger = LogManager.getLogger(HandlersModule.class);
-
-    @Provides @Singleton @Named("sensorFilterHandler")
-    public FilterHandler<Observation> provideFilterHandler(StatisticsConfigRepository repository) {
-        logger.info("Creating a new instance for the handler '{}'.", "sensorFilterHandler");
-        return new FilterHandler<Observation>() {
-            @Override protected List<Sensor> loadMapping() {
-                return repository.getAllAvailableSensors(); }
-            @Override protected Observation newData(Sensor sensor, Observation data) {
-                return new Observation(sensor, data.getValue(), data.getTimestamp()); }
-            @Override protected Sensor getSensor(Observation data) {
-                return data.getSource(); }
-        };
-    }
-
-    @Provides @Singleton @Named("sensorThresholdHandler")
-    public ThresholdHandler<Observation> provideSensorThresholdHandler(StatisticsConfigRepository repository) {
-        logger.info("Creating a new instance for the handler '{}'.", "sensorThresholdHandler");
-        return new ThresholdHandler<Observation>() {
-            @Override protected List<Threshold> loadThresholdValues() {
-                return repository.getCurrentThresholdsValue(); }
-            @Override protected long getGroupId(Observation data) {
-                return data.getSource().getGroupId(); }
-        };
-    }
-
-    @Provides @Singleton @Named("groupThresholdHandler")
-    public ThresholdHandler<DoubleStatistics> provideGroupThresholdHandler(StatisticsConfigRepository repository) {
-        logger.info("Creating a new instance for the handler '{}'.", "groupThresholdHandler");
-        return new ThresholdHandler<DoubleStatistics>() {
-            @Override protected List<Threshold> loadThresholdValues() {
-                return repository.getIntervalThresholdsValue(); }
-            @Override protected long getGroupId(DoubleStatistics data) {
-                return data.getSource().getId(); }
-        };
-    }
-
-    @Provides @Singleton @Named("aggregationCollectorHandler")
-    public BlockingHandler<Observation, DoubleStatistics> provideObservationCollector(
-            StatisticsConfigRepository statisticsConfigRepository,
-            CollectedStatisticsRepository collectedStatisticsRepository,
-            TimestampRepository timestampRepository
-    ) {
-        logger.info("Creating a new instance for the handler '{}'.", "aggregationCollectorHandler");
-        CollectedStatisticsStorage statisticsStorage = CollectedStatisticsStorage.createContext(collectedStatisticsRepository);
-        TimestampStorage timestampStorage = TimestampStorage.createContext(timestampRepository);
-        return new CollectorHandler<Observation>(statisticsStorage, timestampStorage) {
-            @Override protected List<Group> loadGroups() {
-                return statisticsConfigRepository.getAllAvailableGroupInfos(); }
-            @Override protected Function<Observation, Boolean> collectData(DoubleStatistics statistics) {
-                return val -> statistics.accept(val.getSource(), val.getValue()); }
-            @Override protected long getGroupId(Observation data) {
-                return data.getSource().getGroupId(); }
-        };
-    }
-}

+ 0 - 53
src/main/java/cz/senslog/analyzer/analysis/module/ThresholdHandler.java

@@ -1,53 +0,0 @@
-package cz.senslog.analyzer.analysis.module;
-
-import cz.senslog.analyzer.domain.*;
-import cz.senslog.analyzer.core.api.Handler;
-import cz.senslog.analyzer.core.api.HandlerContext;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.*;
-
-import static java.util.Collections.emptyList;
-
-public abstract class ThresholdHandler<T extends Data<?, ?>> extends Handler<T, T> {
-
-    private static final Logger logger = LogManager.getLogger(ThresholdHandler.class);
-
-    /** Map of thresholds rules order by group_id (Map<group_id, List<ThresholdRule>>). */
-    private Map<Long, List<Threshold.Rule>> thresholdRules;
-
-    protected abstract List<Threshold> loadThresholdValues();
-    protected abstract long getGroupId(T data);
-
-    @Override
-    public void init() {
-        List<Threshold> thresholds = loadThresholdValues();
-        thresholdRules = new HashMap<>(thresholds.size());
-        for (Threshold threshold : thresholds) {
-            thresholdRules.computeIfAbsent(threshold.getGroupId(), k -> new ArrayList<>())
-                    .add(threshold.getRule());
-        }
-    }
-
-    @Override
-    public void handle(HandlerContext<T, T> context) {
-        T data = context.data();
-        long groupId = getGroupId(context.data());
-        List<Threshold.Rule> rules = getThresholdRulesByGroupId(groupId);
-        ValidationResult validateResult = data.validate(rules);
-
-        if (validateResult.isNotValid()) {
-            context.eventBus().notify(new Alert(
-                    groupId, String.format("group(%d)", groupId), data.getTimestamp(),
-                    validateResult.getMessages().toArray(new String[0])
-            ));
-        }
-
-        context.next(data);
-    }
-
-    private List<Threshold.Rule> getThresholdRulesByGroupId(long groupId) {
-        return thresholdRules.getOrDefault(groupId, emptyList());
-    }
-}

+ 0 - 127
src/main/java/cz/senslog/analyzer/app/Application.java

@@ -1,127 +0,0 @@
-package cz.senslog.analyzer.app;
-
-import cz.senslog.analyzer.analysis.Analyzer;
-import cz.senslog.analyzer.analysis.DaggerAnalyzerComponent;
-import cz.senslog.analyzer.core.config.ConfigurationModule;
-import cz.senslog.analyzer.core.config.FileConfigurationManager;
-import cz.senslog.analyzer.domain.Observation;
-import cz.senslog.analyzer.provider.DataProviderComponent;
-import cz.senslog.analyzer.storage.ConnectionModule;
-import cz.senslog.analyzer.provider.DaggerDataProviderComponent;
-import cz.senslog.analyzer.provider.DataProvider;
-import cz.senslog.analyzer.storage.ConnectionModule_Factory;
-import cz.senslog.analyzer.ws.DaggerServerComponent;
-import cz.senslog.analyzer.ws.Server;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.io.IOException;
-import java.lang.management.ManagementFactory;
-import java.lang.management.RuntimeMXBean;
-
-import static cz.senslog.analyzer.core.config.ConfigurationType.LOCAL_FILE;
-
-/**
- * The class {@code Application} represents a trigger for entire application.
- *
- * @author Lukas Cerny
- * @version 1.0
- * @since 1.0
- */
-public class Application extends Thread {
-
-    private static final long START_TIMESTAMP;
-    private static final RuntimeMXBean RUNTIME_MX_BEAN;
-
-    private static final Logger logger = LogManager.getLogger(Application.class);
-
-    private final Parameters params;
-
-    static {
-        START_TIMESTAMP = System.currentTimeMillis();
-        RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
-    }
-
-    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;
-    }
-
-    public static long uptime() {
-        return System.currentTimeMillis() - START_TIMESTAMP;
-    }
-
-    public static long uptimeJVM() {
-        return RUNTIME_MX_BEAN.getUptime();
-    }
-
-    private Application(Parameters parameters) {
-        super("app");
-
-        this.params = parameters;
-    }
-
-    @Override
-    public void interrupt() {}
-
-    @Override
-    public void run() {
-
-        String configFile = params.getConfigFileName();
-        FileConfigurationManager configManager = null;
-        try {
-            configManager = FileConfigurationManager.load(configFile);
-        } catch (IOException e) {
-            logger.error(e.getMessage());
-            System.exit(1);
-        }
-
-        ConfigurationModule configurationModule = null;
-        try {
-            configurationModule = ConfigurationModule.create(LOCAL_FILE, configFile);
-        } catch (IOException e) {
-            logger.error(e.getMessage());
-            System.exit(1);
-        }
-
-        ConnectionModule connectionModule = ConnectionModule_Factory.newInstance(configManager.getStorage());
-        logger.info("Module '{}' was created successfully.", connectionModule.getClass().getSimpleName());
-
-        Analyzer<Observation> analyzer = DaggerAnalyzerComponent.builder()
-                .connectionModule(connectionModule)
-                .configurationModule(configurationModule)
-                .build().createObservationAnalyzer();
-        logger.info("Component '{}' was created successfully.", analyzer.getClass().getSimpleName());
-
-
-        DataProviderComponent dataProviderComponent = DaggerDataProviderComponent.builder()
-                .connectionModule(connectionModule)
-                .build();
-
-        DataProvider<?, ?> scheduledDataProvider = dataProviderComponent.scheduledDatabaseProvider()
-                .config(configManager.getScheduler()).deployAnalyzer(analyzer);
-        logger.info("Component '{}' was created successfully.", scheduledDataProvider.getClass().getSimpleName());
-
-        DataProvider<?, ?> loopedDataProvider = dataProviderComponent.loopedDatabaseProvider()
-                .config(configManager.getTaskAnalytics()).deployAnalyzer(analyzer);
-
-        Server server = DaggerServerComponent.builder()
-                .connectionModule(connectionModule)
-                .build().createServer();
-        logger.info("Component '{}' was created successfully.", server.getClass().getSimpleName());
-
-        server.start(configManager.getServer().getPort());
-
-        scheduledDataProvider.start();
-        loopedDataProvider.start();
-
-    }
-}

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

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

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

@@ -1,78 +0,0 @@
-package cz.senslog.analyzer.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.analyzer.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();
-    }
-}

+ 0 - 13
src/main/java/cz/senslog/analyzer/broker/AbstractBrokerConfig.java

@@ -1,13 +0,0 @@
-package cz.senslog.analyzer.broker;
-
-public abstract class AbstractBrokerConfig {
-    private final BrokerType type;
-
-    protected AbstractBrokerConfig(BrokerType type) {
-        this.type = type;
-    }
-
-    public BrokerType getType() {
-        return type;
-    }
-}

+ 0 - 39
src/main/java/cz/senslog/analyzer/broker/AlertFormatter.java

@@ -1,39 +0,0 @@
-package cz.senslog.analyzer.broker;
-
-import cz.senslog.analyzer.domain.Alert;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
-
-public final class AlertFormatter {
-
-    private static final Map<String, Function<Alert, String>> MAP_PROPERTIES;
-
-    static {
-        MAP_PROPERTIES = new HashMap<>();
-        MAP_PROPERTIES.put("$group_id", a -> String.valueOf(a.getGroupId()));
-        MAP_PROPERTIES.put("$group_name", Alert::getGroupName);
-        MAP_PROPERTIES.put("$timestamp", a -> a.getTimestamp().format());
-        MAP_PROPERTIES.put("$messages", a -> Arrays.toString(a.getMessages()));
-    }
-
-    public static Map<String, String> formatToProperties(Alert alert) {
-        Map<String, String> result = new HashMap<>(MAP_PROPERTIES.size());
-        for (Map.Entry<String, Function<Alert, String>> varEntry : MAP_PROPERTIES.entrySet()) {
-            result.put(varEntry.getKey(), varEntry.getValue().apply(alert));
-        }
-        return result;
-    }
-
-    public static String format(Alert alert, String pattern) {
-        String msg = pattern;
-        for (Map.Entry<String, Function<Alert, String>> varEntry : MAP_PROPERTIES.entrySet()) {
-            if (msg.contains(varEntry.getKey())) {
-                msg = msg.replace(varEntry.getKey(), varEntry.getValue().apply(alert));
-            }
-        }
-        return msg;
-    }
-}

+ 0 - 5
src/main/java/cz/senslog/analyzer/broker/BrokerType.java

@@ -1,5 +0,0 @@
-package cz.senslog.analyzer.broker;
-
-public enum BrokerType {
-    DATABASE, EMAIL
-}

+ 0 - 28
src/main/java/cz/senslog/analyzer/broker/database/DatabaseBroker.java

@@ -1,28 +0,0 @@
-package cz.senslog.analyzer.broker.database;
-
-import cz.senslog.analyzer.broker.AlertFormatter;
-import cz.senslog.analyzer.core.AbstractEventBroker;
-import cz.senslog.analyzer.domain.Alert;
-import cz.senslog.analyzer.storage.permanent.repository.SensLogRepository;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-public class DatabaseBroker extends AbstractEventBroker<Alert> {
-
-    private final SensLogRepository repository;
-    private final DatabaseBrokerConfig config;
-
-    public DatabaseBroker(SensLogRepository repository, DatabaseBrokerConfig config) {
-        this.repository = repository;
-        this.config = config;
-    }
-
-    @Override
-    protected void process(Alert value) {
-        repository.saveAlert(AlertFormatter.format(value, config.getPattern()));
-    }
-}

+ 0 - 18
src/main/java/cz/senslog/analyzer/broker/database/DatabaseBrokerConfig.java

@@ -1,18 +0,0 @@
-package cz.senslog.analyzer.broker.database;
-
-import cz.senslog.analyzer.broker.AbstractBrokerConfig;
-import cz.senslog.analyzer.broker.BrokerType;
-
-public class DatabaseBrokerConfig extends AbstractBrokerConfig {
-
-    private final String pattern;
-
-    public DatabaseBrokerConfig(String pattern) {
-        super(BrokerType.DATABASE);
-        this.pattern = pattern;
-    }
-
-    public String getPattern() {
-        return pattern;
-    }
-}

+ 0 - 38
src/main/java/cz/senslog/analyzer/broker/email/EmailBroker.java

@@ -1,38 +0,0 @@
-package cz.senslog.analyzer.broker.email;
-
-import cz.senslog.analyzer.broker.AlertFormatter;
-import cz.senslog.analyzer.core.AbstractEventBroker;
-import cz.senslog.analyzer.domain.Alert;
-import jakarta.mail.MessagingException;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-public class EmailBroker extends AbstractEventBroker<Alert> {
-
-    private static final Logger logger = LogManager.getLogger(EmailBroker.class);
-
-    private final EmailServerConnection connection;
-
-    private final EmailMessageConfig messageConfig;
-
-    public EmailBroker(EmailServerConfig serverConfig, EmailMessageConfig messageConfig) {
-        if (!serverConfig.getName().equalsIgnoreCase(messageConfig.getServerName())) {
-            throw new RuntimeException(String.format("Message broker configuration uses the wrong email server. It has '%s' but expected '%s'.",
-                   serverConfig.getName(), messageConfig.getServerName()));
-        }
-        this.connection = new EmailServerConnection(serverConfig);
-        this.messageConfig = messageConfig;
-    }
-
-    @Override
-    protected void process(Alert alert) {
-
-        String message = AlertFormatter.format(alert, "'$group_name' failed at $timestamp in $messages.");
-
-        try {
-            connection.send(message, AlertFormatter.formatToProperties(alert), messageConfig);
-        } catch (MessagingException e) {
-            throw new RuntimeException(e);
-        }
-    }
-}

+ 0 - 38
src/main/java/cz/senslog/analyzer/broker/email/EmailMessageConfig.java

@@ -1,38 +0,0 @@
-package cz.senslog.analyzer.broker.email;
-
-import cz.senslog.analyzer.broker.AbstractBrokerConfig;
-import cz.senslog.analyzer.broker.BrokerType;
-
-import java.util.Set;
-
-public class EmailMessageConfig extends AbstractBrokerConfig {
-
-    private final String serverName;
-    private final String senderEmail;
-    private final Set<String> recipientEmails;
-    private final String subject;
-
-    public EmailMessageConfig(String serverName, String senderEmail, Set<String> recipientEmails, String subject) {
-        super(BrokerType.EMAIL);
-        this.serverName = serverName;
-        this.senderEmail = senderEmail;
-        this.recipientEmails = recipientEmails;
-        this.subject = subject;
-    }
-
-    public String getServerName() {
-        return serverName;
-    }
-
-    public String getSenderEmail() {
-        return senderEmail;
-    }
-
-    public Set<String> getRecipientEmails() {
-        return recipientEmails;
-    }
-
-    public String getSubject() {
-        return subject;
-    }
-}

+ 0 - 38
src/main/java/cz/senslog/analyzer/broker/email/EmailServerConfig.java

@@ -1,38 +0,0 @@
-package cz.senslog.analyzer.broker.email;
-
-public class EmailServerConfig {
-
-    private final String name;
-    private final String smtpHost;
-    private final int smtpPort;
-    private final String username;
-    private final String password;
-
-    public EmailServerConfig(String name, String smtpHost, int smtpPort, String username, String password) {
-        this.name = name;
-        this.smtpHost = smtpHost;
-        this.smtpPort = smtpPort;
-        this.username = username;
-        this.password = password;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public String getSmtpHost() {
-        return smtpHost;
-    }
-
-    public int getSmtpPort() {
-        return smtpPort;
-    }
-
-    public String getUsername() {
-        return username;
-    }
-
-    public String getPassword() {
-        return password;
-    }
-}

+ 0 - 61
src/main/java/cz/senslog/analyzer/broker/email/EmailServerConnection.java

@@ -1,61 +0,0 @@
-package cz.senslog.analyzer.broker.email;
-
-import jakarta.mail.*;
-import jakarta.mail.internet.InternetAddress;
-import jakarta.mail.internet.MimeBodyPart;
-import jakarta.mail.internet.MimeMessage;
-import jakarta.mail.internet.MimeMultipart;
-import java.util.Map;
-import java.util.Properties;
-
-import static jakarta.mail.Message.RecipientType.TO;
-
-public class EmailServerConnection {
-
-    private final EmailServerConfig config;
-
-    public EmailServerConnection(EmailServerConfig config) {
-        this.config = config;
-    }
-
-    private Session openSession(EmailServerConfig config) {
-        Properties props = new Properties();
-        props.put("mail.smtp.host", config.getSmtpHost());
-        props.put("mail.smtp.port", config.getSmtpPort());
-        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.getPassword());
-            }
-        };
-        return Session.getInstance(props, auth);
-    }
-
-    public void send(String content, Map<String, String> subjectVariables, EmailMessageConfig messageConfig) throws MessagingException {
-        Session session = openSession(config);
-        Message message = new MimeMessage(session);
-        message.setFrom(new InternetAddress(messageConfig.getSenderEmail()));
-        String recipients = String.join(",",messageConfig.getRecipientEmails());
-        message.setRecipients(TO, InternetAddress.parse(recipients));
-
-        String subjectPattern = messageConfig.getSubject();
-        for (Map.Entry<String, String> varEntry : subjectVariables.entrySet()) {
-            if (subjectPattern.contains(varEntry.getKey())) {
-                subjectPattern = subjectPattern.replace(varEntry.getKey(), varEntry.getValue());
-            }
-        }
-        message.setSubject(subjectPattern);
-
-        MimeBodyPart mimeBodyPart = new MimeBodyPart();
-        mimeBodyPart.setContent(content, "text/html;charset=UTF-8");
-
-        Multipart multipart = new MimeMultipart();
-        multipart.addBodyPart(mimeBodyPart);
-        message.setContent(multipart);
-
-        Transport.send(message);
-    }
-}

+ 0 - 19
src/main/java/cz/senslog/analyzer/broker/statistic/StatisticBroker.java

@@ -1,19 +0,0 @@
-package cz.senslog.analyzer.broker.statistic;
-
-import cz.senslog.analyzer.core.AbstractEventBroker;
-import cz.senslog.analyzer.domain.DoubleStatistics;
-import cz.senslog.analyzer.storage.permanent.repository.StatisticsRepository;
-
-public class StatisticBroker extends AbstractEventBroker<DoubleStatistics> {
-
-    private final StatisticsRepository repository;
-
-    public StatisticBroker(StatisticsRepository repository) {
-        this.repository = repository;
-    }
-
-    @Override
-    protected void process(DoubleStatistics value) {
-        repository.save(value);
-    }
-}

+ 0 - 40
src/main/java/cz/senslog/analyzer/core/AbstractEventBroker.java

@@ -1,40 +0,0 @@
-package cz.senslog.analyzer.core;
-
-import java.util.Collection;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingDeque;
-
-public abstract class AbstractEventBroker<T> {
-
-    private final BlockingQueue<T> queue;
-
-    protected AbstractEventBroker() {
-        this.queue = new LinkedBlockingDeque<>();
-        new Thread(this::take).start();
-    }
-
-    protected abstract void process(T value);
-
-    private void take() {
-        while(true) {
-            try {
-                process(queue.take());
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            }
-        }
-    }
-
-    public void put(T value) {
-        try {
-            queue.put(value);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public void put(Collection<T> values) {
-        values.forEach(this::put);
-    }
-
-}

+ 0 - 63
src/main/java/cz/senslog/analyzer/core/AbstractWatchableObjects.java

@@ -1,63 +0,0 @@
-package cz.senslog.analyzer.core;
-
-import cz.senslog.analyzer.core.api.WatchableObject;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class AbstractWatchableObjects<T extends WatchableObject> implements WatchableObjects<T> {
-
-    private static class SavedObject<T extends WatchableObject> {
-        private final T value;
-        private final int identity;
-        private final int originalHash;
-        private int currentHash;
-
-        private SavedObject(T value) {
-            this.value = value;
-            this.identity = System.identityHashCode(value);
-            this.originalHash = value.hash();
-            this.currentHash = originalHash;
-        }
-    }
-
-    private final Map<Integer, SavedObject<T>> watchedObjects;
-
-    protected AbstractWatchableObjects() {
-        this.watchedObjects = new HashMap<>();
-    }
-
-    @Override
-    public T watch(T value) {
-        SavedObject<T> savedObject = new SavedObject<>(value);
-        watchedObjects.put(savedObject.identity, savedObject);
-        return value;
-    }
-
-    @Override
-    public T remove(T value) {
-        SavedObject<T> savedObject = watchedObjects.remove(System.identityHashCode(value));
-        return savedObject == null ? null : savedObject.value;
-    }
-
-    @Override
-    public List<T> changes() {
-        List<T> changed = new ArrayList<>(watchedObjects.size());
-        for (SavedObject<T> obj : watchedObjects.values()) {
-            if (obj.currentHash != obj.value.hash()) {
-                changed.add(obj.value);
-            }
-        }
-        return changed;
-    }
-
-    @Override
-    public boolean commit() {
-        for (SavedObject<T> obj : watchedObjects.values()) {
-            obj.currentHash = obj.value.hash();
-        }
-        return true;
-    }
-}

+ 0 - 15
src/main/java/cz/senslog/analyzer/core/EventBus.java

@@ -1,15 +0,0 @@
-package cz.senslog.analyzer.core;
-
-import cz.senslog.analyzer.domain.Alert;
-import cz.senslog.analyzer.domain.DoubleStatistics;
-
-import java.util.List;
-
-public interface EventBus {
-
-    void notify(Alert alert);
-
-    void save(DoubleStatistics statistics);
-
-    void save(List<DoubleStatistics> statistics);
-}

Some files were not shown because too many files changed in this diff