Lukas Cerny 5 роки тому
батько
коміт
4174d0a22e
79 змінених файлів з 3094 додано та 0 видалено
  1. 9 0
      .gitignore
  2. 49 0
      build.gradle
  3. 3 0
      gradle.properties
  4. 172 0
      gradlew
  5. 84 0
      gradlew.bat
  6. 2 0
      settings.gradle
  7. 24 0
      src/main/java/cz/senslog/analyzer/analysis/AggregationUnit.java
  8. 14 0
      src/main/java/cz/senslog/analyzer/analysis/Analyzer.java
  9. 17 0
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java
  10. 4 0
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerConfigInfo.java
  11. 88 0
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerInfo.java
  12. 88 0
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerModule.java
  13. 31 0
      src/main/java/cz/senslog/analyzer/analysis/Checker.java
  14. 37 0
      src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java
  15. 35 0
      src/main/java/cz/senslog/analyzer/analysis/ThresholdRule.java
  16. 104 0
      src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java
  17. 36 0
      src/main/java/cz/senslog/analyzer/analysis/module/FilterHandler.java
  18. 86 0
      src/main/java/cz/senslog/analyzer/analysis/module/HandlersModule.java
  19. 47 0
      src/main/java/cz/senslog/analyzer/analysis/module/ThresholdHandler.java
  20. 110 0
      src/main/java/cz/senslog/analyzer/app/Application.java
  21. 6 0
      src/main/java/cz/senslog/analyzer/app/Constants.java
  22. 10 0
      src/main/java/cz/senslog/analyzer/app/Main.java
  23. 78 0
      src/main/java/cz/senslog/analyzer/app/Parameters.java
  24. 15 0
      src/main/java/cz/senslog/analyzer/core/EventBus.java
  25. 42 0
      src/main/java/cz/senslog/analyzer/core/EventBusModule.java
  26. 27 0
      src/main/java/cz/senslog/analyzer/core/api/BlockingHandler.java
  27. 26 0
      src/main/java/cz/senslog/analyzer/core/api/Builder.java
  28. 25 0
      src/main/java/cz/senslog/analyzer/core/api/ContextBuilder.java
  29. 24 0
      src/main/java/cz/senslog/analyzer/core/api/ContextBusBuilder.java
  30. 10 0
      src/main/java/cz/senslog/analyzer/core/api/DataFinisher.java
  31. 16 0
      src/main/java/cz/senslog/analyzer/core/api/Handler.java
  32. 65 0
      src/main/java/cz/senslog/analyzer/core/api/HandlerContext.java
  33. 29 0
      src/main/java/cz/senslog/analyzer/core/api/HandlerInvoker.java
  34. 8 0
      src/main/java/cz/senslog/analyzer/core/api/HandlerRunnable.java
  35. 23 0
      src/main/java/cz/senslog/analyzer/core/api/NextInvokerBuilder.java
  36. 23 0
      src/main/java/cz/senslog/analyzer/domain/Alert.java
  37. 61 0
      src/main/java/cz/senslog/analyzer/domain/Data.java
  38. 134 0
      src/main/java/cz/senslog/analyzer/domain/DoubleStatistics.java
  39. 22 0
      src/main/java/cz/senslog/analyzer/domain/FilteredSensor.java
  40. 21 0
      src/main/java/cz/senslog/analyzer/domain/Group.java
  41. 9 0
      src/main/java/cz/senslog/analyzer/domain/GroupBy.java
  42. 21 0
      src/main/java/cz/senslog/analyzer/domain/Interval.java
  43. 25 0
      src/main/java/cz/senslog/analyzer/domain/Observation.java
  44. 37 0
      src/main/java/cz/senslog/analyzer/domain/Sensor.java
  45. 32 0
      src/main/java/cz/senslog/analyzer/domain/Threshold.java
  46. 93 0
      src/main/java/cz/senslog/analyzer/domain/Timestamp.java
  47. 29 0
      src/main/java/cz/senslog/analyzer/domain/ValidationResult.java
  48. 29 0
      src/main/java/cz/senslog/analyzer/persistence/AttributeCode.java
  49. 15 0
      src/main/java/cz/senslog/analyzer/persistence/Connection.java
  50. 54 0
      src/main/java/cz/senslog/analyzer/persistence/ConnectionModule.java
  51. 35 0
      src/main/java/cz/senslog/analyzer/persistence/DatabaseConfig.java
  52. 36 0
      src/main/java/cz/senslog/analyzer/persistence/RepositoryModule.java
  53. 27 0
      src/main/java/cz/senslog/analyzer/persistence/SensorIdConverter.java
  54. 45 0
      src/main/java/cz/senslog/analyzer/persistence/repository/SenslogRepository.java
  55. 94 0
      src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsConfigRepository.java
  56. 148 0
      src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsRepository.java
  57. 17 0
      src/main/java/cz/senslog/analyzer/provider/DataProvider.java
  58. 18 0
      src/main/java/cz/senslog/analyzer/provider/DataProviderComponent.java
  59. 8 0
      src/main/java/cz/senslog/analyzer/provider/DataProviderDeployment.java
  60. 16 0
      src/main/java/cz/senslog/analyzer/provider/HttpMiddlewareProvider.java
  61. 18 0
      src/main/java/cz/senslog/analyzer/provider/HttpMiddlewareProviderModule.java
  62. 4 0
      src/main/java/cz/senslog/analyzer/provider/MiddlewareDataProviderConfig.java
  63. 55 0
      src/main/java/cz/senslog/analyzer/provider/ProviderConfiguration.java
  64. 20 0
      src/main/java/cz/senslog/analyzer/provider/ScheduleDatabaseProviderModule.java
  65. 6 0
      src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfig.java
  66. 26 0
      src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfigImpl.java
  67. 59 0
      src/main/java/cz/senslog/analyzer/provider/ScheduledDatabaseProvider.java
  68. 17 0
      src/main/java/cz/senslog/analyzer/server/AbstractHandler.java
  69. 8 0
      src/main/java/cz/senslog/analyzer/server/Server.java
  70. 14 0
      src/main/java/cz/senslog/analyzer/server/ServerComponent.java
  71. 13 0
      src/main/java/cz/senslog/analyzer/server/ServerModule.java
  72. 51 0
      src/main/java/cz/senslog/analyzer/server/handler/InfoHandler.java
  73. 134 0
      src/main/java/cz/senslog/analyzer/server/handler/StatisticsHandler.java
  74. 24 0
      src/main/java/cz/senslog/analyzer/server/vertx/ExceptionHandler.java
  75. 24 0
      src/main/java/cz/senslog/analyzer/server/vertx/VertxAbstractHandler.java
  76. 19 0
      src/main/java/cz/senslog/analyzer/server/vertx/VertxHandlersModule.java
  77. 81 0
      src/main/java/cz/senslog/analyzer/server/vertx/VertxServer.java
  78. 2 0
      src/main/resources/project.properties
  79. 26 0
      src/test/java/cz/senslog/analyzer/persistence/SensorIdConverterTest.java

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+.idea
+*.iml
+bin
+logs
+target
+.gradle
+build
+gradle
+

+ 49 - 0
build.gradle

@@ -0,0 +1,49 @@
+plugins {
+    id 'java'
+}
+
+group 'cz.senslog'
+version '1.0-SNAPSHOT'
+
+sourceCompatibility = 1.8
+
+repositories {
+    mavenCentral()
+    mavenLocal()
+}
+
+test {
+    useJUnitPlatform()
+}
+
+//jar {
+//    manifest {
+//        attributes(
+//                'Main-Class': 'cz.senslog.analyzer.app.Main'
+//        )
+//    }
+//    from {
+//        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+//    }
+//}
+
+dependencies {
+    testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.6.0'
+    testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.0'
+
+    compile group: 'cz.senslog', name: 'common', version: '1.0.0'
+    compile group: 'com.beust', name: 'jcommander', version: '1.78'
+
+    compile group: 'io.vertx', name: 'vertx-core', version: '3.8.5'
+    compile group: 'io.vertx', name: 'vertx-web', version: '3.8.5'
+
+    compile group: 'org.jdbi', name: 'jdbi3-postgres', version: '3.12.2'
+    compile group: 'org.jdbi', name: 'jdbi3-jodatime2', version: '3.12.2'
+    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.4.2'
+    compile group: 'org.postgresql', name: 'postgresql', version: '42.2.10'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.13.1'
+
+
+    implementation 'com.google.dagger:dagger:2.26'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.26'
+}

+ 3 - 0
gradle.properties

@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx10248m -XX:MaxPermSize=256m
+org.gradle.workers.max=4
+org.gradle.parallel=true

+ 172 - 0
gradlew

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

+ 84 - 0
gradlew.bat

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

+ 2 - 0
settings.gradle

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

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

@@ -0,0 +1,24 @@
+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;
+    }
+}

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

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

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

@@ -0,0 +1,17 @@
+package cz.senslog.analyzer.analysis;
+
+import dagger.Component;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+@Singleton
+@Component(modules = AnalyzerModule.class)
+public interface AnalyzerComponent {
+
+    @Named("advancedAnalyzer")
+    Analyzer createAdvancedAnalyzer();
+
+    @Named("simpleAnalyzer")
+    Analyzer createSimpleAnalyzer();
+}

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

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

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

@@ -0,0 +1,88 @@
+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 String name;
+
+        private Status status;
+
+        private 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;
+    }
+}

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

@@ -0,0 +1,88 @@
+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 javax.inject.Named;
+
+import java.util.List;
+
+import static cz.senslog.analyzer.core.api.HandlerInvoker.cancelInvoker;
+
+
+@Module(includes = {
+        HandlersModule.class,
+        EventBusModule.class
+})
+public class AnalyzerModule {
+
+    @Provides @Named("advancedAnalyzer")
+    Analyzer provideAdvancedAnalyzer (
+            @Named("sensorFilterHandler") FilterHandler<Observation> sensorFilterHandler,
+            @Named("sensorThresholdHandler") ThresholdHandler<Observation> thresholdHandler,
+            @Named("observationCollector") BlockingHandler<Observation, DoubleStatistics> observationCollector,
+            @Named("groupFilterHandler") FilterHandler<DoubleStatistics> groupFilterHandler,
+            @Named("groupThresholdHandler") ThresholdHandler<DoubleStatistics> groupThresholdHandler,
+            @Named("statisticsCollector") BlockingHandler<DoubleStatistics, DoubleStatistics> statisticsCollector,
+            EventBus eventBus
+    ) {
+        HandlerInvoker<Observation> invoker = HandlerInvoker.create()
+                .handler(sensorFilterHandler)
+                .nextHandlerInvoker(HandlerInvoker.create()
+                        .handler(thresholdHandler)
+                        .nextHandlerInvoker(HandlerInvoker.create()
+                                .blockingHandler(observationCollector, eventBus::save)
+                                .nextHandlerInvoker(HandlerInvoker.create()
+                                        .handler(groupFilterHandler)
+                                        .nextHandlerInvoker(HandlerInvoker.create()
+                                                .handler(groupThresholdHandler)
+                                                .nextHandlerInvoker(HandlerInvoker.create()
+                                                        .blockingHandler(statisticsCollector, eventBus::save)
+                                                        .nextHandlerInvoker(cancelInvoker())
+                                                        .eventBus(eventBus)
+                                                        .build()
+                                                ).eventBus(eventBus)
+                                                .build()
+                                        ).eventBus(eventBus)
+                                        .build()
+                                ).eventBus(eventBus)
+                                .build()
+                        ).eventBus(eventBus)
+                        .build()
+                ).eventBus(eventBus)
+                .build();
+
+        return new ObservationAnalyzer(invoker);
+    }
+
+    @Provides @Named("simpleAnalyzer")
+    Analyzer provideSimpleAnalyzer (
+            @Named("sensorFilterHandler") FilterHandler<Observation> sensorFilterHandler,
+            @Named("sensorThresholdHandler") ThresholdHandler<Observation> thresholdHandler,
+            @Named("observationCollector") BlockingHandler<Observation, DoubleStatistics> observationCollector,
+            EventBus eventBus
+    ) {
+        HandlerInvoker<Observation> obsColl = HandlerInvoker.create()
+                .blockingHandler(observationCollector, eventBus::save)
+                .nextHandlerInvoker(cancelInvoker()).eventBus(eventBus)
+                .build();
+
+        HandlerInvoker<Observation> obsThs = HandlerInvoker.create().handler(thresholdHandler)
+                .nextHandlerInvoker(obsColl).eventBus(eventBus).build();
+
+        HandlerInvoker<Observation> obsFlt = HandlerInvoker.create().handler(sensorFilterHandler)
+                .nextHandlerInvoker(obsThs).eventBus(eventBus).build();
+
+        return new ObservationAnalyzer(obsFlt);
+    }
+}

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

@@ -0,0 +1,31 @@
+package cz.senslog.analyzer.analysis;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class Checker {
+
+    private static 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(ThresholdRule rule, Double value) {
+        if (rule == null || value == null) return false;
+        return checkThresholdValue(rule.getMode(), value, rule.getValue());
+    }
+
+}

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

@@ -0,0 +1,37 @@
+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 {
+
+    private static 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) {
+        logger.info("Processing new {} observations.", observations.size());
+        invoker.accept(observations);
+        logger.info("Processing of {} observations was finished.", observations.size());
+    }
+
+    @Override
+    public AnalyzerInfo info() {
+        return null;
+    }
+
+    @Override
+    public AnalyzerConfigInfo reloadConfig(List<Object> moduleConfigs) {
+        return new AnalyzerConfigInfo();
+    }
+}

+ 35 - 0
src/main/java/cz/senslog/analyzer/analysis/ThresholdRule.java

@@ -0,0 +1,35 @@
+package cz.senslog.analyzer.analysis;
+
+public class ThresholdRule {
+
+    private final String mode;
+    private final String property;
+    private final Double value;
+
+    public ThresholdRule(String mode, String property, Double value) {
+        this.mode = mode;
+        this.property = property;
+        this.value = value;
+    }
+
+    public String getMode() {
+        return mode;
+    }
+
+    public String getProperty() {
+        return property;
+    }
+
+    public Double getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "ThresholdRule{" +
+                "mode='" + mode + '\'' +
+                ", property='" + property + '\'' +
+                ", value=" + value +
+                '}';
+    }
+}

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

@@ -0,0 +1,104 @@
+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 java.util.*;
+import java.util.function.Consumer;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+public abstract class CollectorHandler<I extends Data<?>> extends BlockingHandler<I, DoubleStatistics> {
+
+    private static class CollectedStatistics {
+        private final Timestamp startTime;
+        private final Timestamp endTime;
+        private final DoubleStatistics summaryStatistics;
+
+        private CollectedStatistics(Sensor sensor, Timestamp startTime, Long interval) {
+            this.startTime = startTime;
+            this.endTime = Timestamp.of(startTime.get().plusSeconds(interval));
+            this.summaryStatistics = new DoubleStatistics(sensor, startTime, interval);
+        }
+    }
+
+    private Map<Long, List<Long>> groupsInterval;
+    private Map<Long, Map<Long, List<CollectedStatistics>>> collectedStatistics;
+
+    public CollectorHandler() {
+        this.collectedStatistics = new HashMap<>();
+    }
+
+    protected abstract List<Group> loadGroups();
+    protected abstract Consumer<I> collectData(DoubleStatistics statistics);
+
+
+    @Override
+    public void init() {
+        List<Group> groups = loadGroups();
+        groupsInterval = new HashMap<>(groups.size());
+        for (Group group : groups) {
+            groupsInterval.computeIfAbsent(group.getId(), k -> new ArrayList<>())
+                    .add(group.getInterval());
+        }
+    }
+
+    @Override
+    public void finish(DataFinisher<DoubleStatistics> finisher, Timestamp edgeDateTime) {
+        Map<Long, Map<Long, List<CollectedStatistics>>> collectedStatistics = getCollectedStatistics();
+        List<DoubleStatistics> finishedData = new ArrayList<>();
+        for (Map<Long, List<CollectedStatistics>> intervals : collectedStatistics.values()) {
+            for (List<CollectedStatistics> statistics : intervals.values()) {
+                Iterator<CollectedStatistics> statisticsIterator = statistics.iterator();
+                while (statisticsIterator.hasNext()) {
+                    CollectedStatistics statistic = statisticsIterator.next();
+                    if (statistic.endTime.isBefore(edgeDateTime)) {
+                        finishedData.add(statistic.summaryStatistics);
+                        statisticsIterator.remove();
+                    }
+                }
+            }
+        }
+        finisher.finish(finishedData);
+    }
+
+    @Override
+    public void handle(HandlerContext<I, DoubleStatistics> context) {
+        I data = context.data();
+        Sensor sensor = data.getSensor();
+        Timestamp timestamp = data.getTimestamp();
+
+        List<Long> intervals = groupsInterval.getOrDefault(sensor.getSensorId(), emptyList());
+        Map<Long, List<CollectedStatistics>> sensorStatistics = getCollectedStatistics()
+                .computeIfAbsent(sensor.getSensorId(), k -> new HashMap<>());
+
+        for (Long interval : intervals) {
+
+            List<CollectedStatistics> statistics = sensorStatistics.computeIfAbsent(interval, k ->
+                            new ArrayList<>(singletonList(new CollectedStatistics(sensor, timestamp, interval))));
+
+            boolean founded = false; // TODO refactor
+            for (CollectedStatistics st : statistics) {
+                if (timestamp.isEqual(st.startTime) ||
+                        (timestamp.isAfter(st.startTime) && timestamp.isBefore(st.endTime))
+                ) {
+                    collectData(st.summaryStatistics).accept(data);
+                    founded = true;
+                }
+            }
+
+            if (!founded) {
+                CollectedStatistics st = new CollectedStatistics(sensor, timestamp, interval);
+                collectData(st.summaryStatistics).accept(data);
+                statistics.add(st);
+            }
+        }
+    }
+
+    private Map<Long, Map<Long, List<CollectedStatistics>>> getCollectedStatistics() {
+        return collectedStatistics;
+    }
+}

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

@@ -0,0 +1,36 @@
+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.FilteredSensor;
+import cz.senslog.analyzer.domain.Sensor;
+
+import java.util.*;
+
+import static java.util.Collections.emptySet;
+
+public abstract class FilterHandler<T extends Data<?>> extends Handler<T, T> {
+
+    private Map<Long, Set<Long>> sensorMapping;
+
+    protected abstract List<FilteredSensor> loadMapping();
+    protected abstract T newData(Sensor sensor, T data);
+
+    @Override
+    public void init() {
+        List<FilteredSensor> sensors = loadMapping();
+        sensorMapping = new HashMap<>(sensors.size());
+        for (FilteredSensor sensor : sensors) {
+            sensorMapping.computeIfAbsent(sensor.getId(), k -> new HashSet<>())
+                    .add(sensor.getNewId());
+        }
+    }
+
+    @Override
+    public void handle(HandlerContext<T, T> context) {
+        Sensor sensor = context.data().getSensor();
+        sensorMapping.getOrDefault(sensor.getSensorId(), emptySet()).forEach(newId ->
+                context.next(newData(new Sensor(newId), context.data())));
+    }
+}

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

@@ -0,0 +1,86 @@
+package cz.senslog.analyzer.analysis.module;
+
+import cz.senslog.analyzer.core.api.BlockingHandler;
+import cz.senslog.analyzer.domain.*;
+import cz.senslog.analyzer.persistence.RepositoryModule;
+import cz.senslog.analyzer.persistence.repository.StatisticsConfigRepository;
+import dagger.Module;
+import dagger.Provides;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+import java.util.*;
+import java.util.function.Consumer;
+
+import static cz.senslog.analyzer.app.Constants.MINIMAL_INTERVAL;
+import static java.util.stream.Collectors.toList;
+
+@Module(includes = RepositoryModule.class)
+public class HandlersModule {
+
+    @Provides @Singleton @Named("sensorFilterHandler")
+    public FilterHandler<Observation> provideFilterHandler(StatisticsConfigRepository repository) {
+        return new FilterHandler<Observation>() {
+            @Override protected List<FilteredSensor> loadMapping() { return repository.getAllAvailableSensors(); }
+            @Override protected Observation newData(Sensor sensor, Observation data) {
+                return new Observation(sensor, data.getValue(), data.getTimestamp());
+            }
+        };
+    }
+    
+    @Provides @Singleton @Named("groupFilterHandler")
+    public FilterHandler<DoubleStatistics> provideGroupFilterHandler(StatisticsConfigRepository repository) {
+        return new FilterHandler<DoubleStatistics>() {
+            @Override protected List<FilteredSensor> loadMapping() {
+                return repository.getAllGroupsMapping();
+            }
+            @Override protected DoubleStatistics newData(Sensor sensor, DoubleStatistics data) {
+                return new DoubleStatistics(sensor, data.getValue(), data.getTimestamp(), data.getInterval());
+            }
+        };
+    }
+
+    @Provides @Singleton @Named("sensorThresholdHandler")
+    public ThresholdHandler<Observation> provideSensorThresholdHandler(StatisticsConfigRepository repository) {
+        return new ThresholdHandler<Observation>() {
+            @Override protected List<Threshold> loadThresholdValues() {
+                return repository.getCurrentThresholdsValue();
+            }
+        };
+    }
+
+    @Provides @Singleton @Named("groupThresholdHandler")
+    public ThresholdHandler<DoubleStatistics> provideGroupThresholdHandler(StatisticsConfigRepository repository) {
+        return new ThresholdHandler<DoubleStatistics>() {
+            @Override protected List<Threshold> loadThresholdValues() {
+                return repository.getIntervalThresholdsValue();
+            }
+        };
+    }
+
+    @Provides @Singleton @Named("observationCollector")
+    public BlockingHandler<Observation, DoubleStatistics> provideObservationCollector(StatisticsConfigRepository repository) {
+        return new CollectorHandler<Observation>() {
+            @Override protected List<Group> loadGroups() {
+                return repository.getAllAvailableSensors().stream()
+                        .map(s -> new Group(s.getId(), MINIMAL_INTERVAL))
+                        .collect(toList());
+            }
+            @Override protected Consumer<Observation> collectData(DoubleStatistics statistics) {
+                return val -> statistics.accept(val.getValue());
+            }
+        };
+    }
+
+    @Provides @Singleton @Named("statisticsCollector")
+    public BlockingHandler<DoubleStatistics, DoubleStatistics> provideStatisticsCollector(StatisticsConfigRepository repository) {
+        return new CollectorHandler<DoubleStatistics>() {
+            @Override protected List<Group> loadGroups() {
+                return repository.getAllIntervalGroups();
+            }
+            @Override protected Consumer<DoubleStatistics> collectData(DoubleStatistics statistics) {
+                return val -> statistics.accept(val.getValue());
+            }
+        };
+    }
+}

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

@@ -0,0 +1,47 @@
+package cz.senslog.analyzer.analysis.module;
+
+import cz.senslog.analyzer.analysis.*;
+import cz.senslog.analyzer.domain.Alert;
+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 cz.senslog.analyzer.domain.Threshold;
+import cz.senslog.analyzer.domain.ValidationResult;
+
+import java.util.*;
+
+import static java.util.Collections.emptyList;
+
+public abstract class ThresholdHandler<T extends Data<?>> extends Handler<T, T> {
+
+    private Map<Long, List<ThresholdRule>> thresholdRules;
+
+    protected abstract List<Threshold> loadThresholdValues();
+
+    @Override
+    public void init() {
+        List<Threshold> thresholds = loadThresholdValues();
+        thresholdRules = new HashMap<>(thresholds.size());
+        for (Threshold threshold : thresholds) {
+            thresholdRules.computeIfAbsent(threshold.getId(), k -> new ArrayList<>())
+                    .add(new ThresholdRule(threshold.getMode(), threshold.getProperty(), threshold.getValue()));
+        }
+    }
+
+    @Override
+    public void handle(HandlerContext<T, T> context) {
+        T data = context.data();
+        Sensor sensor = data.getSensor();
+        List<ThresholdRule> rules = thresholdRules.getOrDefault(sensor.getSensorId(), emptyList());
+        ValidationResult validateResult = data.validate(rules);
+
+        if (validateResult.isNotValid()) {
+            context.eventBus().send(Alert
+                    .create(String.format("Data %s does not match the rules:\n %s", data, validateResult.getMessages()))
+            );
+        }
+
+        context.next(data);
+    }
+}

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

@@ -0,0 +1,110 @@
+package cz.senslog.analyzer.app;
+
+import cz.senslog.analyzer.analysis.Analyzer;
+import cz.senslog.analyzer.analysis.DaggerAnalyzerComponent;
+import cz.senslog.analyzer.persistence.ConnectionModule;
+import cz.senslog.analyzer.persistence.DatabaseConfig;
+import cz.senslog.analyzer.provider.ProviderConfiguration;
+import cz.senslog.analyzer.provider.DaggerDataProviderComponent;
+import cz.senslog.analyzer.provider.DataProvider;
+import cz.senslog.analyzer.server.DaggerServerComponent;
+import cz.senslog.analyzer.server.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 java.time.LocalDateTime;
+
+/**
+ * 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 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();
+
+        DatabaseConfig dbConfig = new DatabaseConfig(
+                "jdbc:postgresql://localhost:5432/postgres",
+                "postgres",
+                "postgres",
+                6
+        );
+
+        int port = 8090;
+
+
+        ConnectionModule connectionModule = ConnectionModule.create(dbConfig);
+
+
+        ProviderConfiguration config = ProviderConfiguration.config()
+                .period(5)
+                .startDateTime(LocalDateTime.of(2019, 2, 10, 2, 0))
+                .get();
+
+        Analyzer analyzer = DaggerAnalyzerComponent.builder()
+                .connectionModule(connectionModule)
+                .build().createSimpleAnalyzer();
+
+        DataProvider dataProvider = DaggerDataProviderComponent.builder()
+                .connectionModule(connectionModule).build()
+                .scheduledDatabaseProvider()
+                .config(config).deployAnalyzer(analyzer);
+
+        Server server = DaggerServerComponent.builder()
+                .connectionModule(connectionModule).build()
+                .createServer();
+
+        server.start(port);
+        dataProvider.start();
+    }
+}

+ 6 - 0
src/main/java/cz/senslog/analyzer/app/Constants.java

@@ -0,0 +1,6 @@
+package cz.senslog.analyzer.app;
+
+public class Constants {
+
+    public static final long MINIMAL_INTERVAL = 60;
+}

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

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

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

@@ -0,0 +1,78 @@
+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.common.util.StringUtils.isNotBlank;
+import static java.lang.String.format;
+import static java.nio.file.Files.notExists;
+import static java.nio.file.Paths.get;
+
+/**
+ * The class {@code Parameters} represents input parameters from
+ * the applications. For parsing is used {@see JCommander} library.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class Parameters {
+
+    private static 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();
+    }
+}

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

@@ -0,0 +1,15 @@
+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 send(Alert alert);
+
+    void save(DoubleStatistics statistics);
+
+    void save(List<DoubleStatistics> statistics);
+}

+ 42 - 0
src/main/java/cz/senslog/analyzer/core/EventBusModule.java

@@ -0,0 +1,42 @@
+package cz.senslog.analyzer.core;
+
+import cz.senslog.analyzer.domain.Alert;
+import cz.senslog.analyzer.domain.DoubleStatistics;
+import cz.senslog.analyzer.persistence.RepositoryModule;
+import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
+import dagger.Module;
+import dagger.Provides;
+
+import javax.inject.Singleton;
+import java.util.List;
+
+@Module(includes = RepositoryModule.class)
+public class EventBusModule {
+
+    @Provides @Singleton
+    public EventBus provideEventBus(
+            StatisticsRepository statisticsRepository,
+            SenslogRepository senslogRepository
+    ) {
+        return new EventBus() {
+
+            // TODO asynchronous loop queue for all events
+
+            @Override
+            public void send(Alert alert) {
+                senslogRepository.saveAlert(alert);
+            }
+
+            @Override
+            public void save(DoubleStatistics statistics) {
+                statisticsRepository.save(statistics);
+            }
+
+            @Override
+            public void save(List<DoubleStatistics> statistics) {
+                statisticsRepository.save(statistics);
+            }
+        };
+    }
+}

+ 27 - 0
src/main/java/cz/senslog/analyzer/core/api/BlockingHandler.java

@@ -0,0 +1,27 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.domain.Data;
+import cz.senslog.analyzer.domain.Timestamp;
+
+public abstract class BlockingHandler<T extends Data<?>, N extends Data<?>> extends HandlerRunnable<T, N> {
+
+    private DataFinisher<N> finisher;
+
+    public abstract void init();
+    public abstract void handle(HandlerContext<T, N> context);
+    public abstract void finish(DataFinisher<N> finisher, Timestamp edgeDateTime);
+
+    final protected void init(DataFinisher<N> finisher) {
+        this.finisher = finisher;
+        init();
+    }
+
+    @Override
+    final protected void run(HandlerContext<T, N> context) {
+        if (context.isEnd()) {
+            finish(val -> { context.next(val); finisher.finish(val); }, context.data().getTimestamp());
+        } else {
+            handle(context);
+        }
+    }
+}

+ 26 - 0
src/main/java/cz/senslog/analyzer/core/api/Builder.java

@@ -0,0 +1,26 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.core.EventBus;
+import cz.senslog.analyzer.domain.Data;
+
+public interface Builder {
+    <T extends Data<?>> HandlerInvoker<T> build();
+
+    class BuilderImpl implements Builder {
+
+        private final HandlerRunnable<?, ?> handler;
+        private final HandlerInvoker<?> invoker;
+        private final EventBus eventBus;
+
+        public BuilderImpl(HandlerRunnable<?, ?> handler, HandlerInvoker<?> invoker, EventBus eventBus) {
+            this.handler = handler;
+            this.invoker = invoker;
+            this.eventBus = eventBus;
+        }
+
+        @Override
+        public <T extends Data<?>> HandlerInvoker<T> build() {
+            return new HandlerInvoker<>(new HandlerContext(handler, invoker, eventBus));
+        }
+    }
+}

+ 25 - 0
src/main/java/cz/senslog/analyzer/core/api/ContextBuilder.java

@@ -0,0 +1,25 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.domain.Data;
+
+public interface ContextBuilder {
+
+    <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> handler(Handler<A, B> handler);
+
+    <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> blockingHandler(BlockingHandler<A, B> handler, DataFinisher<B> finisher);
+
+    class ContextBuilderImpl implements ContextBuilder {
+
+        @Override
+        public <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> handler(Handler<A, B> handler) {
+            handler.init();
+            return new NextInvokerBuilder.NextInvokerBuilderImpl<>(handler);
+        }
+
+        @Override
+        public <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> blockingHandler(BlockingHandler<A, B> handler, DataFinisher<B> finisher) {
+            handler.init(finisher);
+            return new NextInvokerBuilder.NextInvokerBuilderImpl<>(handler);
+        }
+    }
+}

+ 24 - 0
src/main/java/cz/senslog/analyzer/core/api/ContextBusBuilder.java

@@ -0,0 +1,24 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.core.EventBus;
+
+public interface ContextBusBuilder {
+
+    Builder eventBus(EventBus eventBus);
+
+    class ContextBusBuilderImpl implements ContextBusBuilder {
+
+        private final HandlerRunnable<?, ?> handler;
+        private final HandlerInvoker<?> invoker;
+
+        public ContextBusBuilderImpl(HandlerRunnable<?, ?> handler, HandlerInvoker<?> invoker) {
+            this.handler = handler;
+            this.invoker = invoker;
+        }
+
+        @Override
+        public Builder eventBus(EventBus eventBus) {
+            return new Builder.BuilderImpl(handler, invoker, eventBus);
+        }
+    }
+}

+ 10 - 0
src/main/java/cz/senslog/analyzer/core/api/DataFinisher.java

@@ -0,0 +1,10 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.domain.Data;
+
+import java.util.List;
+
+@FunctionalInterface
+public interface DataFinisher<T extends Data<?>> {
+    void finish(List<T> data);
+}

+ 16 - 0
src/main/java/cz/senslog/analyzer/core/api/Handler.java

@@ -0,0 +1,16 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.domain.Data;
+
+public abstract class Handler<T extends Data<?>, N extends Data<?>> extends HandlerRunnable<T, N> {
+
+    public abstract void init();
+    public abstract void handle(HandlerContext<T, N> context);
+
+    @Override
+    final protected void run(HandlerContext<T, N> context) {
+        if (!context.isEnd()) {
+            handle(context);
+        }
+    }
+}

+ 65 - 0
src/main/java/cz/senslog/analyzer/core/api/HandlerContext.java

@@ -0,0 +1,65 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.core.EventBus;
+import cz.senslog.analyzer.domain.Data;
+import cz.senslog.analyzer.domain.Timestamp;
+
+import java.util.*;
+
+
+public class HandlerContext<D extends Data<?>, N extends Data<?>> {
+
+    private Queue<D> inputQueue;
+    private List<N> collectedList;
+
+    private final HandlerRunnable<D, N> handler;
+    private final HandlerInvoker<N> nextInvoker;
+
+    private final EventBus eventBus;
+
+    protected HandlerContext(HandlerRunnable<D, N> handler, HandlerInvoker<N> nextInvoker, EventBus eventBus) {
+        this.handler = handler;
+        this.nextInvoker = nextInvoker;
+        this.eventBus = eventBus;
+        this.collectedList = new ArrayList<>();
+    }
+
+    public D data() {
+        return inputQueue.peek();
+    }
+
+    public boolean isEnd() {
+        return inputQueue.size() == 1;
+    }
+
+    public void next(N next) {
+        collectedList.add(next);
+    }
+
+    public void next(N [] next) {
+        collectedList.addAll(Arrays.asList(next));
+    }
+
+    public void next(List<N> next) {
+        collectedList.addAll(next);
+    }
+
+    protected void accept(List<D> dataList) {
+        Timestamp timestamp = dataList.isEmpty() ? Timestamp.now() : dataList.get(dataList.size() - 1).getTimestamp();
+
+        inputQueue = new LinkedList<>(dataList);
+        dataList = Collections.emptyList();
+        inputQueue.add(D.empty(timestamp));
+
+        do {
+            handler.run(this);
+            inputQueue.remove();
+        } while (!inputQueue.isEmpty());
+
+        nextInvoker.accept(collectedList);
+    }
+
+    public EventBus eventBus() {
+        return eventBus;
+    }
+}

+ 29 - 0
src/main/java/cz/senslog/analyzer/core/api/HandlerInvoker.java

@@ -0,0 +1,29 @@
+package cz.senslog.analyzer.core.api;
+
+
+import cz.senslog.analyzer.domain.Data;
+
+import java.util.List;
+
+public class HandlerInvoker<T extends Data<?>> {
+
+    public static <T extends Data<?>> HandlerInvoker<T> cancelInvoker() {
+        return new HandlerInvoker<T>(new HandlerContext(null, null, null) {
+            @Override final protected void accept(List dataList) { }
+        });
+    }
+
+    private final HandlerContext<T, ? extends Data<?>> handlerContext;
+
+    protected HandlerInvoker(HandlerContext<T, ? extends Data<?>> handlerContext) {
+        this.handlerContext = handlerContext;
+    }
+
+    public static ContextBuilder create() {
+        return new ContextBuilder.ContextBuilderImpl();
+    }
+
+    public void accept(List<T> observations) {
+        handlerContext.accept(observations);
+    }
+}

+ 8 - 0
src/main/java/cz/senslog/analyzer/core/api/HandlerRunnable.java

@@ -0,0 +1,8 @@
+package cz.senslog.analyzer.core.api;
+
+import cz.senslog.analyzer.domain.Data;
+
+public abstract class HandlerRunnable<D extends Data<?>, N extends Data<?>> {
+
+    protected abstract void run(HandlerContext<D, N> context);
+}

+ 23 - 0
src/main/java/cz/senslog/analyzer/core/api/NextInvokerBuilder.java

@@ -0,0 +1,23 @@
+package cz.senslog.analyzer.core.api;
+
+
+import cz.senslog.analyzer.domain.Data;
+
+public interface NextInvokerBuilder<T extends Data<?>> {
+
+    ContextBusBuilder nextHandlerInvoker(HandlerInvoker<T> handlerInvoker);
+
+    class NextInvokerBuilderImpl<T extends Data<?>> implements NextInvokerBuilder<T> {
+
+        private final HandlerRunnable<?, T> handler;
+
+        public NextInvokerBuilderImpl(HandlerRunnable<?, T> handler) {
+            this.handler = handler;
+        }
+
+        @Override
+        public ContextBusBuilder nextHandlerInvoker(HandlerInvoker<T> invoker) {
+            return new ContextBusBuilder.ContextBusBuilderImpl(handler, invoker);
+        }
+    }
+}

+ 23 - 0
src/main/java/cz/senslog/analyzer/domain/Alert.java

@@ -0,0 +1,23 @@
+package cz.senslog.analyzer.domain;
+
+public class Alert {
+
+    private final String message;
+
+    public static Alert create(String message) {
+        return new Alert(message);
+    }
+
+    private Alert(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    @Override
+    public String toString() {
+        return message;
+    }
+}

+ 61 - 0
src/main/java/cz/senslog/analyzer/domain/Data.java

@@ -0,0 +1,61 @@
+package cz.senslog.analyzer.domain;
+
+import cz.senslog.analyzer.analysis.ThresholdRule;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import static cz.senslog.analyzer.analysis.Checker.checkThresholdValue;
+
+public abstract class Data<T> {
+    private final Map<String, Supplier<Double>> mapping;
+
+    private final Sensor sensor;
+    private final Timestamp timestamp;
+
+    public static <T extends Data<?>> T empty(Timestamp timestamp) {
+        return (T) new Data<T>(new Sensor(-1L), timestamp) {
+            @Override public T getValue() { return null;}
+            @Override public ValidationResult validate(List<ThresholdRule> rules) { return new ValidationResult(); }
+            @Override public String toString() { return "Data.empty"; }
+        };
+    }
+
+    protected Data(Sensor sensor, Timestamp timestamp) {
+        this.sensor = sensor;
+        this.timestamp = timestamp;
+        this.mapping = new HashMap<>();
+    }
+
+    public abstract T getValue();
+
+    final protected void addMapping(String property, Supplier<Double> getter) {
+        mapping.put(property, getter);
+    }
+
+    public ValidationResult validate(List<ThresholdRule> rules) {
+        if (rules == null || rules.isEmpty()) { return new ValidationResult(); }
+
+        ValidationResult result = new ValidationResult();
+        for (ThresholdRule rule : rules) {
+            Double value = mapping.getOrDefault(rule.getProperty(), () -> null).get();
+            if (checkThresholdValue(rule, value)) {
+                result.addMessage(String
+                        .format("%s %s %s", value, rule.getMode(), rule.getValue())
+                );
+            }
+        }
+
+        return result;
+    }
+
+    public Sensor getSensor() {
+        return sensor;
+    }
+
+    public Timestamp getTimestamp() {
+        return timestamp;
+    }
+}

+ 134 - 0
src/main/java/cz/senslog/analyzer/domain/DoubleStatistics.java

@@ -0,0 +1,134 @@
+package cz.senslog.analyzer.domain;
+
+import java.util.*;
+import java.util.stream.DoubleStream;
+
+import static cz.senslog.common.json.BasicJson.objectToJson;
+
+
+public class DoubleStatistics extends Data<DoubleStatistics> {
+
+    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 long interval;
+
+    public DoubleStatistics(Sensor sensor, Timestamp timestamp, long interval) {
+        super(sensor, timestamp);
+        this.interval = interval;
+    }
+
+    public DoubleStatistics(Sensor sensor, DoubleStatistics statistics, Timestamp timestamp, long interval) {
+        this(sensor, statistics.count, statistics.min, statistics.max, statistics.sum, timestamp, interval);
+    }
+
+    public DoubleStatistics(Sensor sensor, long count, double min, double max, double sum, Timestamp timestamp, long interval) {
+        super(sensor, timestamp);
+        this.interval = interval;
+        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;
+            }
+        }
+
+        addMapping("min", this::getMin);
+        addMapping("max", this::getMax);
+        addMapping("avg", this::getAverage);
+    }
+
+    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 getCount() {
+        return this.count;
+    }
+
+    public final double getSum() {
+        double tmp = this.sum + this.sumCompensation;
+        return Double.isNaN(tmp) && Double.isInfinite(this.simpleSum) ? this.simpleSum : tmp;
+    }
+
+    public final double getMin() {
+        return this.min;
+    }
+
+    public final double getMax() {
+        return this.max;
+    }
+
+    public final double getAverage() {
+        return this.getCount() > 0L ? this.getSum() / (double)this.getCount() : 0.0D;
+    }
+
+    public long getInterval() {
+        return interval;
+    }
+
+    @Override
+    public DoubleStatistics getValue() {
+        return this;
+    }
+
+    public void accept(DoubleStatistics other) {
+        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);
+    }
+
+    public void accept(double value) {
+        ++this.count;
+        this.simpleSum += value;
+        this.sumWithCompensation(value);
+        this.min = Math.min(this.min, value);
+        this.max = Math.max(this.max, value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DoubleStatistics that = (DoubleStatistics) o;
+        return getSensor().equals(that.getSensor()) &&
+                getMin() == that.getMin() &&
+                getMax() == that.getMax() &&
+                getSum() == that.getSum() &&
+                getAverage() == that.getAverage() &&
+                getCount() == that.getCount();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getSensor(), getMin(), getMax(), getAverage(), getSum(),getCount());
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 22 - 0
src/main/java/cz/senslog/analyzer/domain/FilteredSensor.java

@@ -0,0 +1,22 @@
+package cz.senslog.analyzer.domain;
+
+
+public class FilteredSensor {
+
+    private final Long id;
+
+    private final Long newId;
+
+    public FilteredSensor(Long id, Long newId) {
+        this.id = id;
+        this.newId = newId;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public Long getNewId() {
+        return newId;
+    }
+}

+ 21 - 0
src/main/java/cz/senslog/analyzer/domain/Group.java

@@ -0,0 +1,21 @@
+package cz.senslog.analyzer.domain;
+
+public class Group {
+
+    private final Long id;
+
+    private final Long interval;
+
+    public Group(Long id, Long interval) {
+        this.id = id;
+        this.interval = interval;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public Long getInterval() {
+        return interval;
+    }
+}

+ 9 - 0
src/main/java/cz/senslog/analyzer/domain/GroupBy.java

@@ -0,0 +1,9 @@
+package cz.senslog.analyzer.domain;
+
+public enum  GroupBy {
+    HOUR, DAY, WEEK, MONTH, YEAR;
+
+    public static GroupBy parse(String groupBy) {
+        return valueOf(groupBy);
+    }
+}

+ 21 - 0
src/main/java/cz/senslog/analyzer/domain/Interval.java

@@ -0,0 +1,21 @@
+package cz.senslog.analyzer.domain;
+
+public enum  Interval {
+    HOUR    (1),
+    DAY     (24),
+
+    ;
+
+    private final long hours;
+    Interval(long hours) {
+        this.hours = hours;
+    }
+
+    public long getHours() {
+        return hours;
+    }
+
+    public static Interval parse(String value) {
+        return valueOf(value);
+    }
+}

+ 25 - 0
src/main/java/cz/senslog/analyzer/domain/Observation.java

@@ -0,0 +1,25 @@
+package cz.senslog.analyzer.domain;
+
+
+import static cz.senslog.common.json.BasicJson.objectToJson;
+
+public class Observation extends Data<Double> {
+
+    private final Double value;
+
+    public Observation(Sensor sensor, Double value, Timestamp timestamp) {
+        super(sensor, timestamp);
+        this.value = value;
+
+        addMapping("val", this::getValue);
+    }
+
+    public Double getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 37 - 0
src/main/java/cz/senslog/analyzer/domain/Sensor.java

@@ -0,0 +1,37 @@
+package cz.senslog.analyzer.domain;
+
+import java.util.Objects;
+
+import static cz.senslog.common.json.BasicJson.objectToJson;
+
+public class Sensor {
+
+    private final Long sensorId;
+
+    public Sensor(Long sensorId) {
+        this.sensorId = sensorId;
+    }
+
+    public Long getSensorId() {
+        return sensorId;
+    }
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Sensor that = (Sensor) o;
+        return sensorId.equals(that.sensorId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(sensorId);
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 32 - 0
src/main/java/cz/senslog/analyzer/domain/Threshold.java

@@ -0,0 +1,32 @@
+package cz.senslog.analyzer.domain;
+
+public class Threshold {
+
+    private final Long id;
+    private final String property;
+    private final String mode;
+    private final Double value;
+
+    public Threshold(Long id, String property, String mode, Double value) {
+        this.id = id;
+        this.property = property;
+        this.mode = mode;
+        this.value = value;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public String getProperty() {
+        return property;
+    }
+
+    public String getMode() {
+        return mode;
+    }
+
+    public Double getValue() {
+        return value;
+    }
+}

+ 93 - 0
src/main/java/cz/senslog/analyzer/domain/Timestamp.java

@@ -0,0 +1,93 @@
+package cz.senslog.analyzer.domain;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.Objects;
+
+public class Timestamp {
+
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
+            .append(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+            .optionalStart().appendOffset("+HH", "+00").optionalEnd()
+            .toFormatter();
+
+    private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder()
+            .append(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
+            .toFormatter();
+
+    private static final DateTimeFormatter TIME_FORMATTER = new DateTimeFormatterBuilder()
+            .append(DateTimeFormatter.ofPattern("HH:mm:ss"))
+            .optionalStart().appendOffset("+HH", "+00").optionalEnd()
+            .toFormatter();
+
+
+    private final ZonedDateTime value;
+
+    public static Timestamp parse(String value) {
+        return of(ZonedDateTime.parse(value, DATE_TIME_FORMATTER));
+    }
+
+    public static Timestamp now() {
+        return of(ZonedDateTime.now());
+    }
+
+    public static Timestamp of(ZonedDateTime value) {
+        return new Timestamp(value);
+    }
+
+    private Timestamp(ZonedDateTime value) {
+        this.value = value;
+    }
+
+    public ZonedDateTime get() {
+        return value;
+    }
+
+    public Instant toInstant() {
+        return value.toInstant();
+    }
+
+    public boolean isEqual(Timestamp t) {
+        return value.isEqual(t.get());
+    }
+
+    public boolean isBefore(Timestamp t) {
+        return value.isBefore(t.get());
+    }
+
+    public boolean isAfter(Timestamp t) {
+        return value.isAfter(t.get());
+    }
+
+    public String format() {
+        return value.format(DATE_TIME_FORMATTER);
+    }
+
+    public String timeFormat() {
+        return value.format(TIME_FORMATTER);
+    }
+
+    public String dateFormat() {
+        return value.format(DATE_FORMATTER);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Timestamp timestamp = (Timestamp) o;
+        return Objects.equals(value, timestamp.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    @Override
+    public String toString() {
+        return format();
+    }
+}

+ 29 - 0
src/main/java/cz/senslog/analyzer/domain/ValidationResult.java

@@ -0,0 +1,29 @@
+package cz.senslog.analyzer.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ValidationResult {
+
+    private List<String> messages;
+
+    public ValidationResult() {
+        this.messages = new ArrayList<>();
+    }
+
+    public boolean isValid() {
+        return messages.isEmpty();
+    }
+
+    public boolean isNotValid() {
+        return !isValid();
+    }
+
+    public List<String> getMessages() {
+        return messages;
+    }
+
+    public void addMessage(String message) {
+        messages.add(message);
+    }
+}

+ 29 - 0
src/main/java/cz/senslog/analyzer/persistence/AttributeCode.java

@@ -0,0 +1,29 @@
+package cz.senslog.analyzer.persistence;
+
+public enum AttributeCode {
+
+    MIN     (0001),
+    MAX     (0002),
+    SUM     (0003),
+    COUNT   (0004),
+
+    ;
+
+    private final int code;
+    AttributeCode(int code) {
+        this.code = code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public static AttributeCode valueOf(int code) {
+        for (AttributeCode value : values()) {
+            if (value.code == code) {
+                return value;
+            }
+        }
+        return null;
+    }
+}

+ 15 - 0
src/main/java/cz/senslog/analyzer/persistence/Connection.java

@@ -0,0 +1,15 @@
+package cz.senslog.analyzer.persistence;
+
+
+public class Connection<T> {
+
+    private final T connection;
+
+    public Connection(T connection) {
+        this.connection = connection;
+    }
+
+    public T get() {
+        return connection;
+    }
+}

+ 54 - 0
src/main/java/cz/senslog/analyzer/persistence/ConnectionModule.java

@@ -0,0 +1,54 @@
+package cz.senslog.analyzer.persistence;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import dagger.Module;
+import dagger.Provides;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdbi.v3.core.Jdbi;
+import org.jdbi.v3.jodatime2.JodaTimePlugin;
+import org.jdbi.v3.postgres.PostgresPlugin;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import javax.inject.Singleton;
+
+@Module
+public class ConnectionModule {
+
+    private static Logger logger = LogManager.getLogger(ConnectionModule.class);
+
+    private final DatabaseConfig config;
+
+    public static ConnectionModule create(DatabaseConfig dbConfig) {
+        return new ConnectionModule(dbConfig);
+    }
+
+    private ConnectionModule(DatabaseConfig dbConfig) {
+        this.config = dbConfig;
+    }
+
+    private Connection<Jdbi> connection;
+
+    @Provides @Singleton
+    public Connection<Jdbi> provideConnection() {
+        if (connection == null) {
+            logger.info("Creating a connection to the database of the class: {}.", Jdbi.class);
+            PGSimpleDataSource ds = new PGSimpleDataSource();
+            ds.setUrl(config.getConnectionUrl());
+            ds.setPassword(config.getPassword());
+            ds.setUser(config.getUsername());
+            ds.setLoadBalanceHosts(true);
+            HikariConfig hc = new HikariConfig();
+            hc.setDataSource(ds);
+            hc.setMaximumPoolSize(config.getConnectionPoolSize());
+
+            connection = new Connection<>(Jdbi
+                    .create(new HikariDataSource(hc))
+                    .installPlugin(new PostgresPlugin())
+                    .installPlugin(new JodaTimePlugin())
+            );
+        }
+        return connection;
+    }
+}

+ 35 - 0
src/main/java/cz/senslog/analyzer/persistence/DatabaseConfig.java

@@ -0,0 +1,35 @@
+package cz.senslog.analyzer.persistence;
+
+public class DatabaseConfig {
+
+    private final String connectionUrl;
+
+    private final String username;
+
+    private final String password;
+
+    private final Integer connectionPoolSize;
+
+    public DatabaseConfig(String connectionUrl, String username, String password, Integer connectionPoolSize) {
+        this.connectionUrl = connectionUrl;
+        this.username = username;
+        this.password = password;
+        this.connectionPoolSize = connectionPoolSize;
+    }
+
+    public String getConnectionUrl() {
+        return connectionUrl;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public Integer getConnectionPoolSize() {
+        return connectionPoolSize;
+    }
+}

+ 36 - 0
src/main/java/cz/senslog/analyzer/persistence/RepositoryModule.java

@@ -0,0 +1,36 @@
+package cz.senslog.analyzer.persistence;
+
+import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.analyzer.persistence.repository.StatisticsConfigRepository;
+import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
+import dagger.Module;
+import dagger.Provides;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdbi.v3.core.Jdbi;
+
+import javax.inject.Singleton;
+
+@Module(includes = ConnectionModule.class)
+public class RepositoryModule {
+
+    private static Logger logger = LogManager.getLogger(RepositoryModule.class);
+
+    @Provides @Singleton
+    public SenslogRepository provideObservationRepository(Connection<Jdbi> connection) {
+        logger.info("Creating a new instance of {}.", SenslogRepository.class);
+        return new SenslogRepository(connection);
+    }
+
+    @Provides @Singleton
+    public StatisticsConfigRepository provideStatisticsConfigRepository(Connection<Jdbi> connection) {
+        logger.info("Creating a new instance of {}.", StatisticsConfigRepository.class);
+        return new StatisticsConfigRepository(connection);
+    }
+
+    @Provides @Singleton
+    public StatisticsRepository provideStatisticsRepository(Connection<Jdbi> connection) {
+        logger.info("Creating a new instance of {}.", StatisticsRepository.class);
+        return new StatisticsRepository(connection);
+    }
+}

+ 27 - 0
src/main/java/cz/senslog/analyzer/persistence/SensorIdConverter.java

@@ -0,0 +1,27 @@
+package cz.senslog.analyzer.persistence;
+
+import cz.senslog.analyzer.domain.Sensor;
+import cz.senslog.common.util.Tuple;
+
+public class SensorIdConverter {
+
+    public static long encodeId(Sensor sensor, AttributeCode property) {
+        long sensorId = sensor.getSensorId();
+        int code = property.getCode();
+        // return (sensorId << 4) + code;
+        return sensorId * 10000 + code;
+    }
+
+    public static Tuple<Sensor, AttributeCode> decodeId(long id) {
+        int code = (int) (id % 10000);
+        long sensorId = id / 10000;
+        /*
+        int code = (int)id & 0b1111;
+        long sensorId = id >> 4;
+        */
+
+        AttributeCode property = AttributeCode.valueOf(code);
+        Sensor sensor = new Sensor(sensorId);
+        return Tuple.of(sensor, property);
+    }
+}

+ 45 - 0
src/main/java/cz/senslog/analyzer/persistence/repository/SenslogRepository.java

@@ -0,0 +1,45 @@
+package cz.senslog.analyzer.persistence.repository;
+
+import cz.senslog.analyzer.domain.Alert;
+import cz.senslog.analyzer.domain.Observation;
+import cz.senslog.analyzer.domain.Sensor;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.analyzer.persistence.Connection;
+import org.jdbi.v3.core.Jdbi;
+
+import javax.inject.Inject;
+import java.time.Instant;
+import java.util.List;
+
+public class SenslogRepository {
+
+    private final Jdbi jdbi;
+
+    @Inject
+    public SenslogRepository(Connection<Jdbi> connection) {
+        this.jdbi = connection.get();
+    }
+
+    public List<Observation> getObservationsFromTime(Instant timestamp) {
+        return jdbi.withHandle(h -> h.createQuery(
+                "SELECT sensor_id as sensorId, observed_value as value, time_stamp as timestamp " +
+                        "FROM public.observations " +
+                        "WHERE time_stamp >= :timestamp " +
+                        "ORDER BY time_stamp LIMIT 100"
+                )
+                    .bind("timestamp", timestamp)
+                    .map((rs, ctx) -> new Observation(
+                            new Sensor(rs.getLong("sensorId")),
+                            rs.getDouble("value"),
+                            Timestamp.parse(rs.getString("timestamp"))
+                    )).list()
+        );
+    }
+
+    public void saveAlert(Alert alert) {
+        String msg = alert.getMessage().substring(0, 99);
+        jdbi.useHandle(h -> h.inTransaction(t -> t.execute(
+                "INSERT INTO public.alerts(alert_id, alert_description) SELECT coalesce(MAX(alert_id),0)+1, ? FROM public.alerts;", msg)
+        ));
+    }
+}

+ 94 - 0
src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsConfigRepository.java

@@ -0,0 +1,94 @@
+package cz.senslog.analyzer.persistence.repository;
+
+import cz.senslog.analyzer.domain.FilteredSensor;
+import cz.senslog.analyzer.domain.Group;
+import cz.senslog.analyzer.domain.Threshold;
+import cz.senslog.analyzer.persistence.Connection;
+import org.jdbi.v3.core.Jdbi;
+
+import javax.inject.Inject;
+import java.util.List;
+
+public class StatisticsConfigRepository {
+
+    private final Jdbi jdbi;
+
+    @Inject
+    public StatisticsConfigRepository(Connection<Jdbi> connection) {
+        this.jdbi = connection.get();
+    }
+
+
+    public List<FilteredSensor> getAllAvailableSensors() {
+        return jdbi.withHandle(h -> h.createQuery(
+                        "SELECT DISTINCT sensor_id AS id FROM statistics.sensors"
+                )
+                        .map((rs, ctx) -> {
+                            Long id = rs.getLong("id");
+                            return new FilteredSensor(id, id);
+                        }).list()
+        );
+    }
+
+    public List<FilteredSensor> getAllGroupsMapping() {
+        return jdbi.withHandle(h -> h.createQuery(
+                    "SELECT s.sensor_id AS id, s.group_id AS new_id " +
+                        "FROM statistics.sensors AS s " +
+                        "JOIN statistics.groups AS g ON g.id = s.group_id " +
+                        "WHERE g.interval IS NOT NULL"
+                )
+                        .map((rs, ctx) -> new FilteredSensor(
+                                rs.getLong("id"),
+                                rs.getLong("new_id")
+                        )).list()
+        );
+    }
+
+    public List<Threshold> getCurrentThresholdsValue() {
+        return jdbi.withHandle(h -> h.createQuery(
+                    "SELECT s.sensor_id AS id, ths.property AS property, ths.mode AS mode, ths.value AS value " +
+                        "FROM statistics.sensors AS s " +
+                        "JOIN statistics.groups AS g ON g.id = s.group_id " +
+                        "JOIN statistics.thresholds AS ths ON ths.group_id = g.id " +
+                        "WHERE g.interval IS NULL"
+                )
+                        .map((rs, ctx) -> new Threshold(
+                                rs.getLong("id"),
+                                rs.getString("property"),
+                                rs.getString("mode"),
+                                rs.getDouble("value")
+                        )).list()
+        );
+    }
+
+    public List<Group> getAllIntervalGroups() {
+        return jdbi.withHandle(h -> h.createQuery(
+                    "SELECT id, interval " +
+                        "FROM statistics.groups " +
+                        "WHERE interval IS NOT NULL"
+                )
+                        .map((rs, ctx) -> new Group(
+                                rs.getLong("id"),
+                                rs.getLong("interval")
+                        )).list()
+        );
+    }
+
+
+
+    public List<Threshold> getIntervalThresholdsValue() {
+        return jdbi.withHandle(h -> h.createQuery(
+                    "SELECT ths.group_id AS id, ths.property AS property, ths.mode AS mode, ths.value AS value " +
+                        "FROM statistics.thresholds AS ths " +
+                        "JOIN statistics.groups AS g ON g.id = ths.group_id " +
+                        "WHERE g.interval IS NOT NULL"
+                )
+                        .map((rs, ctx) -> new Threshold(
+                            rs.getLong("id"),
+                            rs.getString("property"),
+                            rs.getString("mode"),
+                            rs.getDouble("value")
+                        )).list()
+        );
+    }
+}

+ 148 - 0
src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsRepository.java

@@ -0,0 +1,148 @@
+package cz.senslog.analyzer.persistence.repository;
+
+import cz.senslog.analyzer.domain.DoubleStatistics;
+import cz.senslog.analyzer.domain.Observation;
+import cz.senslog.analyzer.domain.Sensor;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.analyzer.persistence.AttributeCode;
+import cz.senslog.analyzer.persistence.Connection;
+import cz.senslog.common.util.TimeRange;
+import cz.senslog.common.util.Tuple;
+import org.jdbi.v3.core.Jdbi;
+import org.jdbi.v3.core.statement.PreparedBatch;
+
+import javax.inject.Inject;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Stream;
+
+import static cz.senslog.analyzer.persistence.AttributeCode.*;
+import static cz.senslog.analyzer.persistence.SensorIdConverter.decodeId;
+import static cz.senslog.analyzer.persistence.SensorIdConverter.encodeId;
+
+import static java.util.stream.Collectors.*;
+
+public class StatisticsRepository {
+
+    private final Jdbi jdbi;
+
+    @Inject
+    public StatisticsRepository(Connection<Jdbi> connection) {
+        this.jdbi = connection.get();
+    }
+
+    public void save(DoubleStatistics statistics) {
+        save(Collections.singletonList(statistics));
+    }
+
+    public void save(List<DoubleStatistics> statistics) {
+        jdbi.withHandle(h -> h.inTransaction(t -> {
+
+           PreparedBatch batch = t.prepareBatch("INSERT INTO statistics.records(sensor_attribute, value, interval, timestamp) " +
+                   "VALUES(:id, :value, :interval, :timestamp)");
+
+            statistics.forEach(st -> batch
+                        .bind("id", encodeId(st.getSensor(), MIN))
+                        .bind("value", st.getMin())
+                        .bind("interval", st.getInterval())
+                        .bind("timestamp", st.getTimestamp().get())
+                    .add()
+                        .bind("id", encodeId(st.getSensor(), MAX))
+                        .bind("value", st.getMax())
+                        .bind("interval", st.getInterval())
+                        .bind("timestamp", st.getTimestamp().get())
+                    .add()
+                        .bind("id", encodeId(st.getSensor(), SUM))
+                        .bind("value", st.getSum())
+                        .bind("interval", st.getInterval())
+                        .bind("timestamp", st.getTimestamp().get())
+                    .add()
+                        .bind("id", encodeId(st.getSensor(), COUNT))
+                        .bind("value", Long.valueOf(st.getCount()).doubleValue())
+                        .bind("interval", st.getInterval())
+                        .bind("timestamp", st.getTimestamp().get())
+                    .add()
+                );
+
+           return batch.execute();
+        }));
+    }
+
+    public List<DoubleStatistics> getByTimeRange(Sensor sensor, TimeRange<Instant> timeRange) {
+        Map<Tuple<Timestamp, Integer>, List<Tuple<AttributeCode, Double>>> result =
+                jdbi.withHandle(h -> h.createQuery(
+                        "SELECT r.sensor_attribute as id, r.value as value, r.interval as interval, r.timestamp as timestamp " +
+                                "FROM statistics.records AS r " +
+                                "WHERE (r.sensor_attribute = :id_min " +
+                                            "OR r.sensor_attribute = :id_max " +
+                                            "OR r.sensor_attribute = :id_sum " +
+                                            "OR r.sensor_attribute = :id_count) " +
+                                    "AND timestamp >= :time_from " +
+                                    "AND (r.timestamp + r.interval * interval '1 second') < :time_to"
+                )
+                    .bind("id_min", encodeId(sensor, MIN))
+                    .bind("id_max", encodeId(sensor, MAX))
+                    .bind("id_sum", encodeId(sensor, SUM))
+                    .bind("id_count", encodeId(sensor, COUNT))
+                    .bind("time_from", timeRange.getFrom())
+                    .bind("time_to", timeRange.getTo())
+                .map((rs, ctx) -> {
+                    AttributeCode code = decodeId(rs.getLong("id")).getItem2();
+                    Double value = rs.getDouble("value");
+                    Tuple<AttributeCode, Double> val = Tuple.of(code, value);
+
+                    Timestamp timestamp = Timestamp.parse(rs.getString("timestamp"));
+                    Integer interval = rs.getInt("interval");
+                    Tuple<Timestamp, Integer> key = Tuple.of(timestamp, interval);
+
+                    return new AbstractMap.SimpleEntry<>(key, val);
+                }).collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList()))));
+
+        // TODO refactor
+        return result.entrySet().stream().filter(e -> e.getValue().size() == 4).flatMap(recordEntry -> {
+            Tuple<Timestamp, Integer> key = recordEntry.getKey();
+            Timestamp timestamp = key.getItem1();
+            Integer interval = key.getItem2();
+
+            double min=0, max=0, sum=0; long count=0;
+            boolean unknown = false;
+            for (Tuple<AttributeCode, Double> valEntry : recordEntry.getValue()) {
+                switch (valEntry.getItem1()) {
+                    case MAX: max = valEntry.getItem2(); break;
+                    case MIN: min = valEntry.getItem2(); break;
+                    case SUM: sum = valEntry.getItem2(); break;
+                    case COUNT: count = valEntry.getItem2().longValue(); break;
+                    default: unknown = true;
+                }
+            }
+
+            return unknown ? Stream.empty() : Stream.of(new DoubleStatistics(sensor, count, min, max, sum, timestamp, interval));
+        }).collect(toList());
+    }
+
+    public List<Observation> getRawByTimeRange(Sensor sensor, TimeRange<Instant> timeRange) {
+        return jdbi.withHandle(h -> h.createQuery("SELECT r.sensor_attribute as id, r.value as value, r.timestamp as timestamp " +
+                        "FROM statistics.records AS r " +
+                        "WHERE (r.sensor_attribute = :id_min " +
+                        "OR r.sensor_attribute = :id_max " +
+                        "OR r.sensor_attribute = :id_sum " +
+                        "OR r.sensor_attribute = :id_count) " +
+                        "AND timestamp >= :time_from " +
+                        "AND (r.timestamp + r.interval * interval '1 second') < :time_to"
+                )
+                    .bind("id_min", encodeId(sensor, MIN))
+                    .bind("id_max", encodeId(sensor, MAX))
+                    .bind("id_sum", encodeId(sensor, SUM))
+                    .bind("id_count", encodeId(sensor, COUNT))
+                    .bind("time_from", timeRange.getFrom())
+                    .bind("time_to", timeRange.getTo())
+                    .map((rs, ctx) -> new Observation(
+                            new Sensor(rs.getLong("id")),
+                            rs.getDouble("value"),
+                            Timestamp.parse(rs.getString("timestamp"))
+                        )
+                    ).list()
+        );
+    }
+}

+ 17 - 0
src/main/java/cz/senslog/analyzer/provider/DataProvider.java

@@ -0,0 +1,17 @@
+package cz.senslog.analyzer.provider;
+
+import cz.senslog.analyzer.analysis.Analyzer;
+
+public abstract class DataProvider {
+
+    protected Analyzer analyzer;
+
+    protected ProviderConfiguration config;
+
+    public void init(Analyzer analyzer, ProviderConfiguration providerConfiguration) {
+        this.analyzer = analyzer;
+        this.config = providerConfiguration;
+    }
+
+    public abstract void start();
+}

+ 18 - 0
src/main/java/cz/senslog/analyzer/provider/DataProviderComponent.java

@@ -0,0 +1,18 @@
+package cz.senslog.analyzer.provider;
+
+import dagger.Component;
+
+import javax.inject.Singleton;
+
+@Singleton
+@Component(modules = {
+        ScheduleDatabaseProviderModule.class,
+        HttpMiddlewareProviderModule.class
+})
+public interface DataProviderComponent {
+
+    ScheduledDataProviderConfig scheduledDatabaseProvider();
+
+    MiddlewareDataProviderConfig httpMiddlewareProvider();
+
+}

+ 8 - 0
src/main/java/cz/senslog/analyzer/provider/DataProviderDeployment.java

@@ -0,0 +1,8 @@
+package cz.senslog.analyzer.provider;
+
+import cz.senslog.analyzer.analysis.Analyzer;
+
+public interface DataProviderDeployment {
+
+    DataProvider deployAnalyzer(Analyzer analyzer);
+}

+ 16 - 0
src/main/java/cz/senslog/analyzer/provider/HttpMiddlewareProvider.java

@@ -0,0 +1,16 @@
+package cz.senslog.analyzer.provider;
+
+import javax.inject.Inject;
+
+public class HttpMiddlewareProvider extends DataProvider {
+
+    @Inject
+    public HttpMiddlewareProvider() {
+
+    }
+
+    @Override
+    public void start() {
+
+    }
+}

+ 18 - 0
src/main/java/cz/senslog/analyzer/provider/HttpMiddlewareProviderModule.java

@@ -0,0 +1,18 @@
+package cz.senslog.analyzer.provider;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class HttpMiddlewareProviderModule {
+
+    @Provides
+    public MiddlewareDataProviderConfig provideMiddlewareProvider(HttpMiddlewareProvider provider) {
+        return new MiddlewareDataProviderConfig() {};
+    }
+
+    @Provides
+    public HttpMiddlewareProvider provideHttpMiddlewareProvider() {
+        return new HttpMiddlewareProvider();
+    }
+}

+ 4 - 0
src/main/java/cz/senslog/analyzer/provider/MiddlewareDataProviderConfig.java

@@ -0,0 +1,4 @@
+package cz.senslog.analyzer.provider;
+
+public interface MiddlewareDataProviderConfig {
+}

+ 55 - 0
src/main/java/cz/senslog/analyzer/provider/ProviderConfiguration.java

@@ -0,0 +1,55 @@
+package cz.senslog.analyzer.provider;
+
+import java.time.LocalDateTime;
+
+public class ProviderConfiguration {
+
+    public interface ConfigurationBuilder {
+
+        ConfigurationBuilder period(int period);
+        ConfigurationBuilder startDateTime(LocalDateTime startDateTime);
+
+        ProviderConfiguration get();
+    }
+
+    public static ConfigurationBuilder config() {
+        return new ConfigurationBuilder() {
+
+            private int period;
+            private LocalDateTime startDateTime;
+
+            @Override
+            public ConfigurationBuilder period(int period) {
+                this.period = period;
+                return this;
+            }
+
+            @Override
+            public ConfigurationBuilder startDateTime(LocalDateTime startDateTime) {
+                this.startDateTime = startDateTime;
+                return this;
+            }
+
+            @Override
+            public ProviderConfiguration get() {
+                return new ProviderConfiguration(period, startDateTime);
+            }
+        };
+    }
+
+    private final int period;
+    private final LocalDateTime startDateTime;
+
+    private ProviderConfiguration(int period, LocalDateTime startDateTime) {
+        this.period = period;
+        this.startDateTime = startDateTime;
+    }
+
+    public int getPeriod() {
+        return period;
+    }
+
+    public LocalDateTime getStartDateTime() {
+        return startDateTime;
+    }
+}

+ 20 - 0
src/main/java/cz/senslog/analyzer/provider/ScheduleDatabaseProviderModule.java

@@ -0,0 +1,20 @@
+package cz.senslog.analyzer.provider;
+
+import cz.senslog.analyzer.persistence.RepositoryModule;
+import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import dagger.Module;
+import dagger.Provides;
+
+@Module(includes = RepositoryModule.class)
+public class ScheduleDatabaseProviderModule {
+
+    @Provides
+    public ScheduledDataProviderConfig provideScheduledProvider(ScheduledDatabaseProvider provider) {
+        return new ScheduledDataProviderConfigImpl(provider);
+    }
+
+    @Provides
+    public ScheduledDatabaseProvider provideScheduleDatabaseProvider(SenslogRepository repository) {
+        return new ScheduledDatabaseProvider(repository);
+    }
+}

+ 6 - 0
src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfig.java

@@ -0,0 +1,6 @@
+package cz.senslog.analyzer.provider;
+
+public interface ScheduledDataProviderConfig {
+
+    DataProviderDeployment config(ProviderConfiguration providerConfiguration);
+}

+ 26 - 0
src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfigImpl.java

@@ -0,0 +1,26 @@
+package cz.senslog.analyzer.provider;
+
+import cz.senslog.analyzer.analysis.Analyzer;
+
+public class ScheduledDataProviderConfigImpl implements ScheduledDataProviderConfig, DataProviderDeployment {
+
+    private final ScheduledDatabaseProvider provider;
+
+    private ProviderConfiguration providerConfiguration;
+
+    public ScheduledDataProviderConfigImpl(ScheduledDatabaseProvider provider) {
+        this.provider = provider;
+    }
+
+    @Override
+    public DataProvider deployAnalyzer(Analyzer analyzer) {
+        provider.init(analyzer, providerConfiguration);
+        return provider;
+    }
+
+    @Override
+    public DataProviderDeployment config(ProviderConfiguration providerConfiguration) {
+        this.providerConfiguration = providerConfiguration;
+        return this;
+    }
+}

+ 59 - 0
src/main/java/cz/senslog/analyzer/provider/ScheduledDatabaseProvider.java

@@ -0,0 +1,59 @@
+package cz.senslog.analyzer.provider;
+
+import cz.senslog.analyzer.analysis.Analyzer;
+import cz.senslog.analyzer.domain.Observation;
+import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.common.util.schedule.Scheduler;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+import java.time.*;
+import java.util.List;
+
+public class ScheduledDatabaseProvider extends DataProvider {
+
+    private static Logger logger = LogManager.getLogger(ScheduledDatabaseProvider.class);
+
+    private final SenslogRepository repository;
+
+    @Inject
+    public ScheduledDatabaseProvider(SenslogRepository repository) {
+        this.repository = repository;
+    }
+
+    @Override
+    public void start() {
+
+        Scheduler.createBuilder()
+                .addTask(new AnalyzerTask(analyzer, repository, config.getStartDateTime()), config.getPeriod())
+        .build().start();
+
+    }
+
+    private static class AnalyzerTask implements Runnable {
+
+        private final SenslogRepository repository;
+        private final Analyzer analyzer;
+
+        private Instant timestamp;
+
+        private AnalyzerTask(Analyzer analyzer, SenslogRepository repository, LocalDateTime startDateTime) {
+            this.analyzer = analyzer;
+            this.repository = repository;
+            this.timestamp = startDateTime.toInstant(OffsetDateTime.now(ZoneId.systemDefault()).getOffset());
+        }
+
+        @Override
+        public void run() {
+            List<Observation> observations = repository.getObservationsFromTime(timestamp);
+
+            if (!observations.isEmpty()) {
+                Observation lObs = observations.get(observations.size() - 1);
+                timestamp = lObs.getTimestamp().get().toInstant().plusSeconds(1);
+            }
+
+            analyzer.accept(observations);
+        }
+    }
+}

+ 17 - 0
src/main/java/cz/senslog/analyzer/server/AbstractHandler.java

@@ -0,0 +1,17 @@
+package cz.senslog.analyzer.server;
+
+
+public abstract class AbstractHandler<T> {
+    
+    public void get(T context)      throws RuntimeException    { notImplementedMethod(context); }
+    public void post(T context)     throws RuntimeException    { notImplementedMethod(context); }
+    public void put(T context)      throws RuntimeException    { notImplementedMethod(context); }
+    public void delete(T context)   throws RuntimeException    { notImplementedMethod(context); }
+    public void patch(T context)    throws RuntimeException    { notImplementedMethod(context); }
+
+    public abstract void exception(T context);
+
+    private static <T> void notImplementedMethod(T context) {
+        throw new NullPointerException("Method not implemented.");
+    }
+}

+ 8 - 0
src/main/java/cz/senslog/analyzer/server/Server.java

@@ -0,0 +1,8 @@
+package cz.senslog.analyzer.server;
+
+public interface Server {
+
+    void start(int port);
+
+    void stop();
+}

+ 14 - 0
src/main/java/cz/senslog/analyzer/server/ServerComponent.java

@@ -0,0 +1,14 @@
+package cz.senslog.analyzer.server;
+
+import dagger.Component;
+
+import javax.inject.Singleton;
+
+@Singleton
+@Component(modules = {
+        ServerModule.class
+})
+public interface ServerComponent {
+
+    Server createServer();
+}

+ 13 - 0
src/main/java/cz/senslog/analyzer/server/ServerModule.java

@@ -0,0 +1,13 @@
+package cz.senslog.analyzer.server;
+
+import cz.senslog.analyzer.server.vertx.VertxHandlersModule;
+import cz.senslog.analyzer.server.vertx.VertxServer;
+import dagger.Binds;
+import dagger.Module;
+
+@Module(includes = VertxHandlersModule.class)
+public abstract class ServerModule {
+
+    @Binds
+    public abstract Server provideVertxServer(VertxServer server);
+}

+ 51 - 0
src/main/java/cz/senslog/analyzer/server/handler/InfoHandler.java

@@ -0,0 +1,51 @@
+package cz.senslog.analyzer.server.handler;
+
+import com.google.gson.JsonObject;
+import cz.senslog.analyzer.app.Application;
+import cz.senslog.analyzer.server.vertx.VertxAbstractHandler;
+import io.vertx.core.http.HttpServerResponse;
+import io.vertx.ext.web.RoutingContext;
+
+import javax.inject.Inject;
+
+import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
+import static cz.senslog.common.json.BasicJson.objectToJson;
+
+public class InfoHandler extends VertxAbstractHandler {
+
+    @Inject
+    public InfoHandler(){}
+
+    @Override
+    public void exception(RoutingContext context) {
+        HttpServerResponse response = context.response();
+
+        response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+
+        JsonObject json = new JsonObject();
+
+        json.addProperty("status", "failed");
+        json.addProperty("message", context.failure().getMessage());
+        json.addProperty("code", 400);
+
+        response.setStatusCode(json.get("code").getAsInt()).end(objectToJson(json));
+    }
+
+    @Override
+    public void get(RoutingContext context) throws RuntimeException {
+        HttpServerResponse response = context.response();
+
+        response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+
+        JsonObject uptime = new JsonObject();
+        uptime.addProperty("app", Application.uptime() / 1000);
+        uptime.addProperty("jvm", Application.uptimeJVM() / 1000);
+
+        JsonObject json = new JsonObject();
+        json.addProperty("status", "ok");
+        json.add("uptime", uptime);
+
+        response.end(objectToJson(json));
+    }
+}

+ 134 - 0
src/main/java/cz/senslog/analyzer/server/handler/StatisticsHandler.java

@@ -0,0 +1,134 @@
+package cz.senslog.analyzer.server.handler;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializer;
+import cz.senslog.analyzer.domain.*;
+import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
+import cz.senslog.analyzer.server.vertx.VertxAbstractHandler;
+import cz.senslog.common.util.TimeRange;
+import io.vertx.core.http.HttpServerResponse;
+import io.vertx.ext.web.RoutingContext;
+
+import javax.inject.Inject;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
+import static java.util.Collections.*;
+
+// TODO test statistic endpoint
+public class StatisticsHandler extends VertxAbstractHandler {
+
+    private final StatisticsRepository repository;
+
+    private static final Gson gson = new GsonBuilder()
+            .registerTypeAdapter(DoubleStatistics.class, (JsonSerializer<DoubleStatistics>) (src, typeOfSrc, context1) -> {
+                JsonObject js = new JsonObject();
+                // js.addProperty("sensor_id", src.getSensor().getSensorId());
+                js.addProperty("time", src.getTimestamp().timeFormat());
+                // js.addProperty("date", src.getTimestamp().dateFormat());
+                // js.addProperty("timestamp", src.getTimestamp().format());
+
+                // js.addProperty("interval", src.getInterval());
+                js.addProperty("min", src.getMin());
+                js.addProperty("max", src.getMax());
+                js.addProperty("avg", src.getAverage());
+                return js;
+            })
+            .registerTypeAdapter(Observation.class, (JsonSerializer<Observation>) (src, typeOfSrc, context1) -> {
+                JsonObject js = new JsonObject();
+                js.addProperty("sensor_id", src.getSensor().getSensorId());
+                js.addProperty("timestamp", src.getTimestamp().format());
+                js.addProperty("value", String.format("%.2f", src.getValue()));
+                return js;
+            })
+            .create();
+
+    @Inject
+    public StatisticsHandler(StatisticsRepository repository) {
+        this.repository = repository;
+    }
+
+
+    @Override
+    public void get(RoutingContext context) throws RuntimeException {
+        HttpServerResponse response = context.response();
+
+        Sensor sensor = new Sensor(Long.parseLong(context.request().getParam("sensor_id")));
+        Timestamp from = Timestamp.parse(context.request().getParam("from"));
+        Timestamp to = Timestamp.parse(context.request().getParam("to"));
+        Interval interval = Interval.parse(context.request().getParam("interval"));
+        GroupBy groupBy = GroupBy.parse(context.request().getParam("groupBy"));
+        TimeRange<Instant> timeRange = TimeRange.of(from.toInstant(), to.toInstant());
+
+        if ((interval != Interval.HOUR && groupBy == GroupBy.DAY) || (interval == Interval.HOUR && groupBy != GroupBy.DAY)) {
+            throw new RuntimeException(String.format(
+                    "Option '%s' and '%s' is not allowed. Combination of '%s' and '%s' is allowed only.",
+                    interval, groupBy, Interval.HOUR, GroupBy.DAY
+            ));
+        }
+
+        String format = context.request().getParam("format").toLowerCase();
+
+        Object result;
+        switch (format) {
+            case "pretty": {
+                List<DoubleStatistics> statistics = repository.getByTimeRange(sensor, timeRange);
+                result = MergeStatistics
+                        .init(sensor, interval, groupBy).merge(statistics);
+            } break;
+            case "raw": {
+                result = repository.getRawByTimeRange(sensor, timeRange);
+            } break;
+            default: {
+                result = Collections.emptyList();
+            }
+        }
+
+        response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+        response.end(gson.toJson(result));
+    }
+
+    private static class MergeStatistics {
+
+        private final Interval interval;
+        private final GroupBy groupBy;
+
+        private static MergeStatistics init(Sensor sensor, Interval interval, GroupBy groupBy){
+            return new MergeStatistics(sensor, interval, groupBy);
+        }
+
+        private MergeStatistics(Sensor sensor, Interval interval, GroupBy groupBy) {
+            this.interval = interval;
+            this.groupBy = groupBy;
+        }
+
+        public Map<String, List<DoubleStatistics>> merge(List<DoubleStatistics> statistics) {
+
+            Map<String, List<DoubleStatistics>> result = emptyMap();
+
+            if (groupBy == GroupBy.DAY) {
+                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+                Map<LocalDate, List<DoubleStatistics>> groupedResults = new HashMap<>();
+
+                for (DoubleStatistics st : statistics) {
+                    groupedResults.computeIfAbsent(st.getTimestamp().get().toLocalDate(), k -> new ArrayList<>())
+                            .add(st);
+                }
+
+                result = new HashMap<>(groupedResults.size());
+                for (Map.Entry<LocalDate, List<DoubleStatistics>> entry : groupedResults.entrySet()) {
+                    result.put(entry.getKey().format(formatter), entry.getValue());
+                }
+            }
+
+            return result;
+        }
+    }
+}
+

+ 24 - 0
src/main/java/cz/senslog/analyzer/server/vertx/ExceptionHandler.java

@@ -0,0 +1,24 @@
+package cz.senslog.analyzer.server.vertx;
+
+import cz.senslog.common.util.function.ThrowingConsumer;
+import io.vertx.core.Handler;
+import io.vertx.ext.web.RoutingContext;
+
+import java.util.function.BiConsumer;
+
+public class ExceptionHandler {
+
+    public static ExceptionHandler create() {
+        return new ExceptionHandler();
+    }
+
+    public Handler<RoutingContext> handle(ThrowingConsumer<RoutingContext, Exception> throwingConsumer, BiConsumer<RoutingContext, Exception> exceptionHandler) {
+        return context -> {
+            try {
+                throwingConsumer.accept(context);
+            } catch (Exception exception) {
+                exceptionHandler.accept(context, exception);
+            }
+        };
+    }
+}

+ 24 - 0
src/main/java/cz/senslog/analyzer/server/vertx/VertxAbstractHandler.java

@@ -0,0 +1,24 @@
+package cz.senslog.analyzer.server.vertx;
+
+import com.google.gson.JsonObject;
+import cz.senslog.analyzer.server.AbstractHandler;
+import cz.senslog.common.http.HttpHeader;
+import io.vertx.ext.web.RoutingContext;
+
+import static cz.senslog.common.json.BasicJson.objectToJson;
+
+public abstract class VertxAbstractHandler extends AbstractHandler<RoutingContext> {
+
+    @Override
+    public void exception(RoutingContext context) {
+        JsonObject json = new JsonObject();
+
+        json.addProperty("message", context.failure().getMessage());
+        json.addProperty("code", 400);
+
+        context.response()
+                .putHeader(HttpHeader.CONTENT_TYPE, "application/json")
+                .setStatusCode(400).end(objectToJson(json));
+    }
+
+}

+ 19 - 0
src/main/java/cz/senslog/analyzer/server/vertx/VertxHandlersModule.java

@@ -0,0 +1,19 @@
+package cz.senslog.analyzer.server.vertx;
+
+import cz.senslog.analyzer.persistence.RepositoryModule;
+import cz.senslog.analyzer.server.handler.InfoHandler;
+import cz.senslog.analyzer.server.handler.StatisticsHandler;
+import dagger.Binds;
+import dagger.Module;
+
+@Module(includes = {
+        RepositoryModule.class
+})
+public abstract class VertxHandlersModule {
+
+    @Binds
+    public abstract VertxAbstractHandler provideExampleHandler(InfoHandler handler);
+
+    @Binds
+    public abstract VertxAbstractHandler provideStatisticsHandler(StatisticsHandler handler);
+}

+ 81 - 0
src/main/java/cz/senslog/analyzer/server/vertx/VertxServer.java

@@ -0,0 +1,81 @@
+package cz.senslog.analyzer.server.vertx;
+
+import cz.senslog.analyzer.server.Server;
+import cz.senslog.analyzer.server.handler.InfoHandler;
+import cz.senslog.analyzer.server.handler.StatisticsHandler;
+import io.vertx.core.*;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+
+
+public class VertxServer extends AbstractVerticle implements Server {
+
+    private static Logger logger = LogManager.getLogger(VertxServer.class);
+
+    private final InfoHandler infoHandler;
+    private final StatisticsHandler statisticsHandler;
+
+    private Vertx vertx;
+
+    @Inject
+    public VertxServer(
+            InfoHandler infoHandler,
+            StatisticsHandler statisticsHandler
+    ) {
+        this.infoHandler = infoHandler;
+        this.statisticsHandler = statisticsHandler;
+    }
+
+    @Override
+    public void start(final Promise<Void> promise) {
+
+        Router router = Router.router(vertx);
+
+        router.route("/info/*").failureHandler(infoHandler::exception);
+        router.get("/info").blockingHandler(infoHandler::get);
+
+        router.route("/statistics/*").failureHandler(statisticsHandler::exception);
+        router.get("/statistics").blockingHandler(statisticsHandler::get);
+
+//        Router mainRouter = Router.router(vertx);
+//        mainRouter.route().handler((ctx)->{
+//            System.out.println("Filtering " + ctx.normalisedPath());
+//            ctx.next();
+//        });
+//        mainRouter.mountSubRouter("/", router);
+
+        vertx.createHttpServer()
+                .requestHandler(router)
+                .listen(config().getInteger("http.server.port"), result -> {
+                    if (result.succeeded()) { promise.complete(); }
+                    else { promise.fail(result.cause()); }
+                });
+    }
+
+    @Override
+    public void stop() {
+        this.vertx.close();
+    }
+
+    @Override
+    public void start(int port) {
+        vertx = Vertx.vertx();
+
+        DeploymentOptions options = new DeploymentOptions().setConfig(new JsonObject()
+                .put("http.server.port", port)
+        );
+
+        vertx.deployVerticle(this, options, res -> {
+            if (res.succeeded()) {
+                logger.info("Deployment id is {} ", res.result());
+                logger.info("The HTTP server running on port {}.", port);
+            } else {
+                logger.error("Could not start the HTTP server: " + res.cause());
+            }
+        });
+    }
+}

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

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

+ 26 - 0
src/test/java/cz/senslog/analyzer/persistence/SensorIdConverterTest.java

@@ -0,0 +1,26 @@
+package cz.senslog.analyzer.persistence;
+
+import cz.senslog.analyzer.domain.Sensor;
+import cz.senslog.common.util.Tuple;
+import org.junit.jupiter.api.Test;
+
+import static cz.senslog.analyzer.persistence.SensorIdConverter.decodeId;
+import static cz.senslog.analyzer.persistence.SensorIdConverter.encodeId;
+import static org.junit.jupiter.api.Assertions.*;
+
+class SensorIdConverterTest {
+
+    @Test
+    void encodePersistenceId_true() {
+        assertEquals(10001, encodeId(new Sensor(1L), AttributeCode.MIN));
+        assertEquals(280004, encodeId(new Sensor(28L), AttributeCode.COUNT));
+
+    }
+
+    @Test
+    void decodePersistenceId_true() {
+        Tuple<Sensor, AttributeCode> id = decodeId(280004);
+        assertEquals(28L, id.getItem1().getSensorId());
+        assertEquals(AttributeCode.COUNT, id.getItem2());
+    }
+}