Browse Source

Changed aggregation logic, add dc files, changed format of rest api

Lukas Cerny 5 năm trước cách đây
mục cha
commit
56a975c2b0
43 tập tin đã thay đổi với 937 bổ sung360 xóa
  1. 1 1
      README.md
  2. 1 1
      build.gradle
  3. 2 2
      config/config.yaml
  4. BIN
      doc/data_loading_processing.png
  5. 1 0
      doc/data_loading_processing.xml
  6. 2 4
      src/main/java/cz/senslog/analyzer/analysis/Analyzer.java
  7. 2 1
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java
  8. 1 1
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerModule.java
  9. 6 4
      src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java
  10. 9 2
      src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java
  11. 7 7
      src/main/java/cz/senslog/analyzer/app/Application.java
  12. 3 1
      src/main/java/cz/senslog/analyzer/domain/CollectedStatistics.java
  13. 4 0
      src/main/java/cz/senslog/analyzer/domain/DoubleStatistics.java
  14. 0 9
      src/main/java/cz/senslog/analyzer/domain/GroupBy.java
  15. 0 23
      src/main/java/cz/senslog/analyzer/domain/Interval.java
  16. 23 0
      src/main/java/cz/senslog/analyzer/domain/IntervalGroup.java
  17. 21 2
      src/main/java/cz/senslog/analyzer/domain/Timestamp.java
  18. 22 0
      src/main/java/cz/senslog/analyzer/provider/AnalyzerTask.java
  19. 3 3
      src/main/java/cz/senslog/analyzer/provider/DataProvider.java
  20. 2 1
      src/main/java/cz/senslog/analyzer/provider/DataProviderComponent.java
  21. 2 2
      src/main/java/cz/senslog/analyzer/provider/DataProviderDeployment.java
  22. 2 2
      src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfig.java
  23. 4 3
      src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfigImpl.java
  24. 17 10
      src/main/java/cz/senslog/analyzer/provider/ScheduledDatabaseProvider.java
  25. 0 221
      src/main/java/cz/senslog/analyzer/server/handler/StatisticsHandler.java
  26. 5 1
      src/main/java/cz/senslog/analyzer/storage/permanent/repository/SensLogRepository.java
  27. 52 34
      src/main/java/cz/senslog/analyzer/storage/permanent/repository/StatisticsConfigRepository.java
  28. 4 4
      src/main/java/cz/senslog/analyzer/storage/permanent/repository/StatisticsRepository.java
  29. 14 0
      src/main/java/cz/senslog/analyzer/util/DateUtils.java
  30. 142 0
      src/main/java/cz/senslog/analyzer/util/TimestampUtil.java
  31. 1 1
      src/main/java/cz/senslog/analyzer/ws/Server.java
  32. 1 1
      src/main/java/cz/senslog/analyzer/ws/ServerComponent.java
  33. 3 3
      src/main/java/cz/senslog/analyzer/ws/ServerModule.java
  34. 94 0
      src/main/java/cz/senslog/analyzer/ws/dto/SensorStatisticsData.java
  35. 2 2
      src/main/java/cz/senslog/analyzer/ws/handler/GroupsHandler.java
  36. 2 4
      src/main/java/cz/senslog/analyzer/ws/handler/InfoHandler.java
  37. 121 0
      src/main/java/cz/senslog/analyzer/ws/handler/StatisticsHandler.java
  38. 110 0
      src/main/java/cz/senslog/analyzer/ws/manager/WSStatisticsManager.java
  39. 1 1
      src/main/java/cz/senslog/analyzer/ws/vertx/AbstractRestHandler.java
  40. 4 4
      src/main/java/cz/senslog/analyzer/ws/vertx/VertxHandlersModule.java
  41. 21 5
      src/main/java/cz/senslog/analyzer/ws/vertx/VertxServer.java
  42. 123 0
      src/test/java/cz/senslog/analyzer/util/TimestampUtilTest.java
  43. 102 0
      src/test/java/cz/senslog/analyzer/ws/manager/WSStatisticsManagerTest.java

+ 1 - 1
README.md

@@ -9,7 +9,7 @@ http://127.0.0.1:9090/statistics?
 group_id=<GROUP_ID>&
 from=<FROM>&
 to=<TO>&
-groupBy=<ENUM_GROUP>
+intervalGroup=<ENUM_GROUP>
 
 
 GROUP_ID -> long number

+ 1 - 1
build.gradle

@@ -29,7 +29,7 @@ jar {
 
 dependencies {
     testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.6.0'
-    testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.0'
+    testCompile group: 'org.mockito', name: 'mockito-core', version: '3.6.28'
 
     compile group: 'cz.senslog', name: 'common', version: '1.0.0'
     compile group: 'com.beust', name: 'jcommander', version: '1.78'

+ 2 - 2
config/config.yaml

@@ -10,8 +10,8 @@ inMemoryStorage:
   parameters: ""
 
 scheduler:
-  initDate: "2020-01-30T14:00:00.00+02:00"
-  period: 30
+  initDate: "1970-01-01T00:00:00.00+02:00"
+  period: 2
 
 server:
   port: 9090

BIN
doc/data_loading_processing.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
doc/data_loading_processing.xml


+ 2 - 4
src/main/java/cz/senslog/analyzer/analysis/Analyzer.java

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

+ 2 - 1
src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java

@@ -1,5 +1,6 @@
 package cz.senslog.analyzer.analysis;
 
+import cz.senslog.analyzer.domain.Observation;
 import dagger.Component;
 
 import javax.inject.Named;
@@ -10,5 +11,5 @@ import javax.inject.Singleton;
 public interface AnalyzerComponent {
 
     @Named("statisticsAnalyzer")
-    Analyzer createNewAnalyzer();
+    Analyzer<Observation> createNewObservationAnalyzer();
 }

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

@@ -29,7 +29,7 @@ public class AnalyzerModule {
     private static final Logger logger = LogManager.getLogger(AnalyzerModule.class);
 
     @Provides @Named("statisticsAnalyzer")
-    Analyzer provideSimpleAnalyzer (
+    Analyzer<Observation> provideSimpleAnalyzer (
             @Named("sensorFilterHandler") FilterHandler<Observation> sensorFilterHandler,
             @Named("sensorThresholdHandler") ThresholdHandler<Observation> sensorThresholdHandler,
             @Named("aggregationCollectorHandler") BlockingHandler<Observation, DoubleStatistics> aggregateCollectorHandler,

+ 6 - 4
src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java

@@ -7,7 +7,7 @@ import org.apache.logging.log4j.Logger;
 
 import java.util.List;
 
-public class ObservationAnalyzer implements Analyzer {
+public class ObservationAnalyzer implements Analyzer<Observation> {
 
     private static final Logger logger = LogManager.getLogger(ObservationAnalyzer.class);
 
@@ -19,9 +19,11 @@ public class ObservationAnalyzer implements Analyzer {
 
     @Override
     public void accept(List<Observation> observations) {
-        logger.info("Processing of the new {} observations.", observations.size());
-        invoker.accept(observations);
-        logger.info("The process of processing new observations is finished.");
+        synchronized (this) {
+            logger.info("Processing of the new {} observations.", observations.size());
+            invoker.accept(observations);
+            logger.info("The process of processing new observations is finished.");
+        }
     }
 
     @Override

+ 9 - 2
src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java

@@ -5,9 +5,11 @@ import cz.senslog.analyzer.core.api.DataFinisher;
 import cz.senslog.analyzer.core.api.HandlerContext;
 import cz.senslog.analyzer.domain.*;
 import cz.senslog.analyzer.storage.inmemory.CollectedStatisticsStorage;
+import cz.senslog.common.util.DateTrunc;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.time.OffsetDateTime;
 import java.util.*;
 import java.util.function.Function;
 
@@ -73,7 +75,7 @@ public abstract class CollectorHandler<I extends Data<?, ?>> extends BlockingHan
         List<CollectedStatistics> groupStatistics = getCollectedStatisticsByGroup(group);
 
         boolean newDataAccepted = false;
-        for (CollectedStatistics st : groupStatistics) {
+        for (CollectedStatistics st : groupStatistics) { // startInterval <= timestamp < endInterval
             if (timestamp.isEqual(st.getStartTime()) ||
                     (timestamp.isAfter(st.getStartTime()) && timestamp.isBefore(st.getEndTime()))
             ) {
@@ -83,12 +85,17 @@ public abstract class CollectorHandler<I extends Data<?, ?>> extends BlockingHan
         }
 
         if (!newDataAccepted) { // register a new statistics
-            CollectedStatistics newSt = new CollectedStatistics(group, timestamp);
+            Timestamp startOfInterval = createStartOfInterval(timestamp, group);
+            CollectedStatistics newSt = new CollectedStatistics(group, startOfInterval);
             collectData(newSt.getStatistics()).apply(data);
             groupStatistics.add(storage.watch(newSt));
         }
     }
 
+    private static Timestamp createStartOfInterval(Timestamp timestamp, Group group) {
+        return Timestamp.of(DateTrunc.trunc(timestamp.get(), (int)group.getInterval()));
+    }
+
     private Group getGroupByGroupId(long groupId) {
         return groupsGroupById.getOrDefault(groupId, Group.empty());
     }

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

@@ -2,14 +2,14 @@ package cz.senslog.analyzer.app;
 
 import cz.senslog.analyzer.analysis.Analyzer;
 import cz.senslog.analyzer.analysis.DaggerAnalyzerComponent;
+import cz.senslog.analyzer.domain.Observation;
 import cz.senslog.analyzer.storage.ConnectionModule;
 import cz.senslog.analyzer.storage.StorageConfig;
-import cz.senslog.analyzer.storage.permanent.PermanentStorageConfig;
 import cz.senslog.analyzer.provider.ProviderConfig;
 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 cz.senslog.analyzer.ws.DaggerServerComponent;
+import cz.senslog.analyzer.ws.Server;
 import cz.senslog.common.util.Triple;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -87,11 +87,11 @@ public class Application extends Thread {
 
         ConnectionModule connectionModule = ConnectionModule.create(storageConfig);
 
-        Analyzer analyzer = DaggerAnalyzerComponent.builder()
+        Analyzer<Observation> analyzer = DaggerAnalyzerComponent.builder()
                 .connectionModule(connectionModule)
-                .build().createNewAnalyzer();
+                .build().createNewObservationAnalyzer();
 
-        DataProvider dataProvider = DaggerDataProviderComponent.builder()
+        DataProvider<Observation> dataProvider = DaggerDataProviderComponent.builder()
                 .connectionModule(connectionModule).build()
                 .scheduledDatabaseProvider()
                 .config(config).deployAnalyzer(analyzer);
@@ -101,6 +101,6 @@ public class Application extends Thread {
                 .createServer();
 
         server.start(port);
-        dataProvider.start();
+       // dataProvider.start();
     }
 }

+ 3 - 1
src/main/java/cz/senslog/analyzer/domain/CollectedStatistics.java

@@ -2,6 +2,8 @@ package cz.senslog.analyzer.domain;
 
 import cz.senslog.analyzer.core.api.WatchableObject;
 
+import static java.time.temporal.ChronoUnit.SECONDS;
+
 public class CollectedStatistics implements WatchableObject {
 
     private final Timestamp startTime, endTime;
@@ -14,7 +16,7 @@ public class CollectedStatistics implements WatchableObject {
     public CollectedStatistics(DoubleStatistics statistics) {
         this.startTime = statistics.getTimestamp();
         long interval = statistics.getSource().getInterval();
-        this.endTime = Timestamp.of(startTime.get().plusSeconds(interval));
+        this.endTime = startTime.plus(interval, SECONDS);
         this.statistics = statistics;
     }
 

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

@@ -24,6 +24,10 @@ public class DoubleStatistics extends Data<Group, DoubleStatistics> {
         initMapping();
     }
 
+    public DoubleStatistics(DoubleStatistics statistics) {
+        this(statistics.getSource(), statistics, statistics.getTimestamp());
+    }
+
     public DoubleStatistics(Group group, DoubleStatistics statistics, Timestamp timestamp) {
         this(group, statistics.count, statistics.min, statistics.max, statistics.sum, timestamp);
     }

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

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

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

@@ -1,23 +0,0 @@
-package cz.senslog.analyzer.domain;
-
-public enum  Interval {
-    HOUR    (1),
-    DAY     (24),
-    WEAK    (168),
-    MONTH   (720),
-
-
-    ;
-    private final long seconds;
-    Interval(long hours) {
-        this.seconds = hours * 3600;
-    }
-
-    public long getSeconds() {
-        return seconds;
-    }
-
-    public static Interval parse(String value) {
-        return value != null ? valueOf(value) : null;
-    }
-}

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

@@ -0,0 +1,23 @@
+package cz.senslog.analyzer.domain;
+
+import java.time.temporal.ChronoUnit;
+
+public enum IntervalGroup {
+    HOUR    (ChronoUnit.HOURS),
+    DAY     (ChronoUnit.DAYS),
+    MONTH   (ChronoUnit.MONTHS),
+    YEAR    (ChronoUnit.YEARS);
+
+    private final ChronoUnit chronoUnit;
+    IntervalGroup(ChronoUnit chronoUnit) {
+        this.chronoUnit = chronoUnit;
+    }
+
+    public ChronoUnit getChronoUnit() {
+        return chronoUnit;
+    }
+
+    public static IntervalGroup parseIntervalGroup(String intervalGroup) {
+        return intervalGroup != null ? valueOf(intervalGroup) : null;
+    }
+}

+ 21 - 2
src/main/java/cz/senslog/analyzer/domain/Timestamp.java

@@ -4,9 +4,11 @@ import java.time.*;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
 import java.util.Objects;
 
-public class Timestamp {
+public class Timestamp implements Comparable<Timestamp> {
 
     private static final DateTimeFormatter INPUT_FORMATTER = new DateTimeFormatterBuilder()
             .append(DateTimeFormatter.ofPattern("yyyy-MM-dd[ HH:mm:ss]"))
@@ -37,7 +39,11 @@ public class Timestamp {
     private final OffsetDateTime value;
 
     public static Timestamp parse(String value) {
-        return of(OffsetDateTime.parse(value, INPUT_FORMATTER));
+        return parse(value, INPUT_FORMATTER);
+    }
+
+    public static Timestamp parse(String value, DateTimeFormatter formatter) {
+        return of(OffsetDateTime.parse(value, formatter));
     }
 
     public static Timestamp now() {
@@ -84,6 +90,14 @@ public class Timestamp {
         return value.format(DATE_FORMATTER);
     }
 
+    public Timestamp minus(int amount, ChronoUnit unit) {
+        return of(value.minus(amount, unit));
+    }
+
+    public Timestamp plus(long amount, ChronoUnit unit) {
+        return amount != 0 ? of(value.plus(amount, unit)) : of(value);
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -101,4 +115,9 @@ public class Timestamp {
     public String toString() {
         return format();
     }
+
+    @Override
+    public int compareTo(Timestamp timestamp) {
+        return value.compareTo(timestamp.value);
+    }
 }

+ 22 - 0
src/main/java/cz/senslog/analyzer/provider/AnalyzerTask.java

@@ -0,0 +1,22 @@
+package cz.senslog.analyzer.provider;
+
+
+import cz.senslog.analyzer.analysis.Analyzer;
+
+import java.util.List;
+
+public abstract class AnalyzerTask<T> implements Runnable {
+
+    private final Analyzer<T> analyzer;
+
+    protected AnalyzerTask(Analyzer<T> analyzer) {
+        this.analyzer = analyzer;
+    }
+
+    protected abstract List<T> loadData();
+
+    @Override
+    public final void run() {
+        this.analyzer.accept(loadData());
+    }
+}

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

@@ -2,13 +2,13 @@ package cz.senslog.analyzer.provider;
 
 import cz.senslog.analyzer.analysis.Analyzer;
 
-public abstract class DataProvider {
+public abstract class DataProvider<T> {
 
-    protected Analyzer analyzer;
+    protected Analyzer<T> analyzer;
 
     protected ProviderConfig config;
 
-    public void init(Analyzer analyzer, ProviderConfig providerConfiguration) {
+    public void init(Analyzer<T> analyzer, ProviderConfig providerConfiguration) {
         this.analyzer = analyzer;
         this.config = providerConfiguration;
     }

+ 2 - 1
src/main/java/cz/senslog/analyzer/provider/DataProviderComponent.java

@@ -1,5 +1,6 @@
 package cz.senslog.analyzer.provider;
 
+import cz.senslog.analyzer.domain.Observation;
 import dagger.Component;
 
 import javax.inject.Singleton;
@@ -11,7 +12,7 @@ import javax.inject.Singleton;
 })
 public interface DataProviderComponent {
 
-    ScheduledDataProviderConfig scheduledDatabaseProvider();
+    ScheduledDataProviderConfig scheduledDatabaseProvider(); // TODO refactor
 
     MiddlewareDataProviderConfig httpMiddlewareProvider();
 

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

@@ -2,7 +2,7 @@ package cz.senslog.analyzer.provider;
 
 import cz.senslog.analyzer.analysis.Analyzer;
 
-public interface DataProviderDeployment {
+public interface DataProviderDeployment<T> {
 
-    DataProvider deployAnalyzer(Analyzer analyzer);
+    DataProvider<T> deployAnalyzer(Analyzer<T> analyzer);
 }

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

@@ -1,6 +1,6 @@
 package cz.senslog.analyzer.provider;
 
-public interface ScheduledDataProviderConfig {
+public interface ScheduledDataProviderConfig<T> {
 
-    DataProviderDeployment config(ProviderConfig providerConfiguration);
+    DataProviderDeployment<T> config(ProviderConfig providerConfiguration);
 }

+ 4 - 3
src/main/java/cz/senslog/analyzer/provider/ScheduledDataProviderConfigImpl.java

@@ -1,8 +1,9 @@
 package cz.senslog.analyzer.provider;
 
 import cz.senslog.analyzer.analysis.Analyzer;
+import cz.senslog.analyzer.domain.Observation;
 
-public class ScheduledDataProviderConfigImpl implements ScheduledDataProviderConfig, DataProviderDeployment {
+public class ScheduledDataProviderConfigImpl implements ScheduledDataProviderConfig<Observation>, DataProviderDeployment<Observation> {
 
     private final ScheduledDatabaseProvider provider;
 
@@ -13,13 +14,13 @@ public class ScheduledDataProviderConfigImpl implements ScheduledDataProviderCon
     }
 
     @Override
-    public DataProvider deployAnalyzer(Analyzer analyzer) {
+    public DataProvider<Observation> deployAnalyzer(Analyzer<Observation> analyzer) {
         provider.init(analyzer, providerConfiguration);
         return provider;
     }
 
     @Override
-    public DataProviderDeployment config(ProviderConfig providerConfiguration) {
+    public DataProviderDeployment<Observation> config(ProviderConfig providerConfiguration) {
         this.providerConfiguration = providerConfiguration;
         return this;
     }

+ 17 - 10
src/main/java/cz/senslog/analyzer/provider/ScheduledDatabaseProvider.java

@@ -13,12 +13,14 @@ import java.time.*;
 import java.util.ArrayList;
 import java.util.List;
 
-public class ScheduledDatabaseProvider extends DataProvider {
+public class ScheduledDatabaseProvider extends DataProvider<Observation> {
 
     private static final Logger logger = LogManager.getLogger(ScheduledDatabaseProvider.class);
 
     private final SensLogRepository repository;
 
+    private Scheduler scheduler;
+
     @Inject
     public ScheduledDatabaseProvider(SensLogRepository repository) {
         this.repository = repository;
@@ -27,27 +29,32 @@ public class ScheduledDatabaseProvider extends DataProvider {
     @Override
     public void start() {
 
-        Scheduler.createBuilder()
-                .addTask(new AnalyzerTask(analyzer, repository, config.getStartDateTime()), config.getPeriod())
-        .build().start();
+        scheduler = Scheduler.createBuilder()
+                .addTask(new ObservationAnalyzerTask(analyzer, repository, config.getStartDateTime()), config.getPeriod())
+        .build();
+
+        scheduler.start();
+
+    }
 
+    private Scheduler registerTask(AnalyzerTask<Observation> mainTask, AnalyzerTask<Observation>... tasks) {
+        return null;
     }
 
-    private static class AnalyzerTask implements Runnable {
+    private static class ObservationAnalyzerTask extends AnalyzerTask<Observation> {
 
         private final SensLogRepository repository;
-        private final Analyzer analyzer;
 
         private OffsetDateTime timestamp;
 
-        private AnalyzerTask(Analyzer analyzer, SensLogRepository repository, OffsetDateTime startDateTime) {
-            this.analyzer = analyzer;
+        private ObservationAnalyzerTask(Analyzer<Observation> analyzer, SensLogRepository repository, OffsetDateTime startDateTime) {
+            super(analyzer);
             this.repository = repository;
             this.timestamp = startDateTime;
         }
 
         @Override
-        public void run() {
+        protected List<Observation> loadData() {
             List<Observation> observations = repository.getObservationsFromTime(timestamp);
 
             if (!observations.isEmpty()) {
@@ -74,7 +81,7 @@ public class ScheduledDatabaseProvider extends DataProvider {
                 timestamp = lastTimestamp.plusSeconds(1);
             }
 
-            analyzer.accept(observations);
+            return observations;
         }
     }
 }

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

@@ -1,221 +0,0 @@
-package cz.senslog.analyzer.server.handler;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSerializer;
-import cz.senslog.analyzer.domain.*;
-import cz.senslog.analyzer.storage.permanent.repository.StatisticsConfigRepository;
-import cz.senslog.analyzer.storage.permanent.repository.StatisticsRepository;
-import cz.senslog.analyzer.server.vertx.AbstractRestHandler;
-import cz.senslog.common.util.TimeRange;
-import cz.senslog.common.util.Tuple;
-import io.vertx.core.MultiMap;
-import io.vertx.core.http.HttpServerResponse;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import javax.inject.Inject;
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.temporal.ChronoUnit;
-import java.util.*;
-
-import static cz.senslog.analyzer.domain.AttributeValue.*;
-import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
-import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
-import static java.time.format.DateTimeFormatter.ofPattern;
-import static java.util.Collections.singletonList;
-
-public class StatisticsHandler extends AbstractRestHandler {
-
-    private static final Logger logger = LogManager.getLogger(StatisticsHandler.class);
-
-    private final StatisticsRepository statisticsRepository;
-    private final StatisticsConfigRepository configRepository;
-
-    private static final Gson gson = new GsonBuilder()
-            .registerTypeAdapter(DoubleStatistics.class, (JsonSerializer<DoubleStatistics>) (src, typeOfSrc, context1) -> {
-                com.google.gson.JsonObject js = new com.google.gson.JsonObject();
-//                js.addProperty("time", src.getTimestamp().timeFormat());
-                // js.addProperty("date", src.getTimestamp().dateFormat());
-                 js.addProperty("timestamp", src.getTimestamp().format());
-
-                js.addProperty(MIN.name().toLowerCase(), src.getMin());
-                js.addProperty(MAX.name().toLowerCase(), src.getMax());
-                js.addProperty(AVG.name().toLowerCase(), src.getAverage());
-                return js;
-            })
-            .create();
-
-    @Inject
-    public StatisticsHandler(
-            StatisticsRepository statisticsRepository,
-            StatisticsConfigRepository configRepository
-    ) {
-        this.statisticsRepository = statisticsRepository;
-        this.configRepository = configRepository;
-    }
-
-    @Override
-    public void start() {
-
-        router().get("/analyticsByUnitSensor").handler(ctx -> {
-            logger.info("Handling '{}' with the params '{}'.", ctx.request().path(), ctx.request().params().entries());
-
-            HttpServerResponse response = ctx.response();
-            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            MultiMap params = ctx.request().params();
-
-            long unitIdParam;
-            Long sensorIdParam;
-            TimeRange<Instant> timeRangeParam;
-            GroupBy groupByParam;
-            Interval intervalParam;
-
-            try {
-                unitIdParam = Long.parseLong(params.get("unit_id"));
-                sensorIdParam = params.contains("sensor_id") ? Long.parseLong(params.get("sensor_id")) : null;
-
-                Timestamp fromParam = Timestamp.parse(params.get("from"));
-                Timestamp toParam = Timestamp.parse(params.get("to"));
-                timeRangeParam = TimeRange.of(fromParam.toInstant(), toParam.toInstant());
-
-                groupByParam = GroupBy.parse(params.get("group_by"));
-                intervalParam = Interval.parse(params.get("interval"));
-            } catch (NumberFormatException | DateTimeParseException e) {
-                response.setStatusCode(404).end(new JsonObject()
-                        .put("message", e.getMessage()).encode()); return;
-            }
-
-            long timeDiffSec = timeRangeParam.difference(ChronoUnit.SECONDS);
-            StatisticsConfigRepository.Special specialRepository = configRepository.special();
-            // List<Tuple<GroupId, SensorId>>
-            List<Tuple<Long, Long>> sensorInGroup;
-            if (sensorIdParam != null) {
-                long groupId = specialRepository.getGroupIdByUnitSensor(unitIdParam, sensorIdParam, timeDiffSec);
-                sensorInGroup = singletonList(Tuple.of(groupId, sensorIdParam));
-            } else {
-                sensorInGroup = specialRepository.getGroupsByUnit(unitIdParam, timeDiffSec);
-            }
-
-            Map<Long, List<DoubleStatistics>> statisticsBySensor = new HashMap<>(sensorInGroup.size());
-            for (Tuple<Long, Long> groupEntry : sensorInGroup) {
-                long groupId = groupEntry.getItem1();
-                long sensorId = groupEntry.getItem2();
-                List<DoubleStatistics> statistics = statisticsRepository.getByTimeRange(groupId, timeRangeParam);
-                statisticsBySensor.put(sensorId, statistics);
-            }
-
-            JsonObject result = new JsonObject();
-            for (Map.Entry<Long, List<DoubleStatistics>> sensorEntry : statisticsBySensor.entrySet()) {
-                String sensorIdStr = sensorEntry.getKey().toString();
-                List<DoubleStatistics> statistics = sensorEntry.getValue();
-
-                if (groupByParam == null) {
-                    DoubleStatistics mergedSt = MergeStatistics.mergeToOne(statistics);
-                    if (mergedSt != null) {
-                        result.put(sensorIdStr, new JsonObject()
-                                .put("min", mergedSt.getMin())
-                                .put("max", mergedSt.getMax())
-                                .put("avg", mergedSt.getAverage())
-                        );
-                    }
-                } else {
-                    Map<String, List<DoubleStatistics>> grouped = MergeStatistics.mergeByGroup(groupByParam, statistics);
-                    if (!grouped.isEmpty()) {
-                        JsonObject groupJson = new JsonObject();
-                        for (Map.Entry<String, List<DoubleStatistics>> entry : grouped.entrySet()) {
-                            JsonArray sensorsSt = new JsonArray();
-                            for (DoubleStatistics st : entry.getValue()) {
-                                sensorsSt.add(new JsonObject()
-                                        .put("min", st.getMin())
-                                        .put("max", st.getMax())
-                                        .put("avg", st.getAverage())
-                                        .put("timestamp", st.getTimestamp().format())
-                                        .put("interval", st.getSource().getInterval())
-                                );
-                            }
-                            groupJson.put(entry.getKey(), sensorsSt);
-                        }
-                        result.put(sensorIdStr, groupJson);
-                    }
-                }
-            }
-
-            response.end(result.encode());
-        });
-
-        router().get("/analyticsByUser").handler(ctx -> {
-            HttpServerResponse response = ctx.response();
-            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            MultiMap params = ctx.request().params();
-
-            long userId = Long.parseLong(params.get("user_id"));
-            Timestamp from = Timestamp.parse(params.get("from"));
-            Timestamp to = Timestamp.parse(params.get("to"));
-            GroupBy groupBy = GroupBy.parse(params.get("group_by"));
-            TimeRange<Instant> timeRange = TimeRange.of(from.toInstant(), to.toInstant());
-
-            response.setStatusCode(501).end(new JsonObject()
-                    .put("message", "not implemented yet")
-                    .encode());
-        });
-
-        router().get("/analyticsByGroup").handler(ctx -> {
-            HttpServerResponse response = ctx.response();
-            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            MultiMap params = ctx.request().params();
-
-            long groupId = Long.parseLong(params.get("group_id"));
-            Timestamp from = Timestamp.parse(params.get("from"));
-            Timestamp to = Timestamp.parse(params.get("to"));
-            GroupBy groupBy = GroupBy.parse(params.get("group_by"));
-            TimeRange<Instant> timeRange = TimeRange.of(from.toInstant(), to.toInstant());
-
-            List<DoubleStatistics> statistics = statisticsRepository.getByTimeRange(groupId, timeRange);
-            Object result;
-            if (groupBy == null) {
-                result = MergeStatistics.mergeToOne(statistics);
-            } else {
-                result = MergeStatistics.mergeByGroup(groupBy, statistics);
-            }
-
-            response.end(gson.toJson(result));
-        });
-    }
-
-    private static class MergeStatistics {
-
-        public static Map<String, List<DoubleStatistics>> mergeByGroup(GroupBy groupBy, List<DoubleStatistics> statistics) {
-            Map<String, List<DoubleStatistics>> result = new HashMap<>();
-            DateTimeFormatter formatter = null;
-
-            switch (groupBy) {
-                case DAY:   formatter = ofPattern("yyyy-MM-dd"); break;
-                case MONTH: formatter = ofPattern("yyyy-MM");    break;
-                case YEAR:  formatter = ofPattern("yyyy");       break;
-            }
-
-            for (DoubleStatistics st : statistics) {
-                String time = st.getTimestamp().get().format(formatter);
-                result.computeIfAbsent(time, k -> new ArrayList<>()).add(st);
-            }
-
-            return result;
-        }
-
-        public static DoubleStatistics mergeToOne(List<DoubleStatistics> statistics) {
-            if (statistics == null || statistics.isEmpty()) {
-                return null;
-            }
-            DoubleStatistics first = statistics.get(0);
-            for (int i = 1; i < statistics.size(); i++) {
-                first.accept(statistics.get(i));
-            }
-            return first;
-        }
-    }
-}

+ 5 - 1
src/main/java/cz/senslog/analyzer/storage/permanent/repository/SensLogRepository.java

@@ -11,10 +11,14 @@ import org.jdbi.v3.core.Jdbi;
 
 import javax.inject.Inject;
 import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 
 public class SensLogRepository {
 
+    private static final DateTimeFormatter POSTGRESQL_TIMESTAMPWTZ_PATTERN =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSX");
+
     private static final Logger logger = LogManager.getLogger(SensLogRepository.class);
 
     private final Jdbi jdbi;
@@ -36,7 +40,7 @@ public class SensLogRepository {
                                     rs.getLong("sensor_id")
                             ),
                             rs.getDouble("observed_value"),
-                            Timestamp.parse(rs.getString("time_stamp"))
+                            Timestamp.parse(rs.getString("time_stamp"), POSTGRESQL_TIMESTAMPWTZ_PATTERN)
                     )).list()
         );
     }

+ 52 - 34
src/main/java/cz/senslog/analyzer/storage/permanent/repository/StatisticsConfigRepository.java

@@ -1,20 +1,28 @@
 package cz.senslog.analyzer.storage.permanent.repository;
 
 import cz.senslog.analyzer.domain.*;
+import cz.senslog.analyzer.provider.ScheduledDatabaseProvider;
 import cz.senslog.analyzer.storage.Connection;
 import cz.senslog.common.util.Tuple;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.jdbi.v3.core.Jdbi;
 
 import javax.inject.Inject;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import java.util.AbstractMap;
 import java.util.List;
 import java.util.Map;
 
+import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 import static java.util.stream.Collectors.*;
 
 public class StatisticsConfigRepository {
 
+    private static final Logger logger = LogManager.getLogger(ScheduledDatabaseProvider.class);
+
     private final Jdbi jdbi;
 
     private final Special specialRepository;
@@ -119,43 +127,53 @@ public class StatisticsConfigRepository {
         }
 
         public List<Tuple<Long, Long>> getGroupsByUnit(long unitId, long maxInterval) {
-            return jdbi.withHandle(h -> h.createQuery(
-                    "SELECT g.id AS group_id, s.sensor_id AS sensor_id " +
-                            "FROM statistics.sensors AS s " +
-                            "JOIN statistics.sensor_to_group AS sg ON sg.sensor_id = s.id " +
-                            "JOIN statistics.groups_interval AS g ON g.id = sg.group_id " +
-                            "WHERE s.unit_id = :unit_id " +
-                            "AND g.time_interval > 0 " +
-                            "AND g.time_interval <= :aggr_interval_s " +
-                            "AND g.persistence = TRUE " +
-                            "AND g.aggregation_type = 'DOUBLE' " +
-                            "ORDER BY g.time_interval DESC"
-                    )
-                            .bind("unit_id", unitId)
-                            .bind("aggr_interval_s", maxInterval)
-                            .bind("aggr_type", AggregationType.DOUBLE)
-                            .map((rs, ctx) ->
-                                    Tuple.of(rs.getLong("group_id"), rs.getLong("sensor_id"))
-                            ).list()
-            );
+            try {
+                return jdbi.<List<Tuple<Long, Long>>, Exception>withHandle(h -> h.createQuery(
+                        "SELECT g.id AS group_id, s.sensor_id AS sensor_id " +
+                                "FROM statistics.sensors AS s " +
+                                "JOIN statistics.sensor_to_group AS sg ON sg.sensor_id = s.id " +
+                                "JOIN statistics.groups_interval AS g ON g.id = sg.group_id " +
+                                "WHERE s.unit_id = :unit_id " +
+                                "AND g.time_interval > 0 " +
+                                "AND g.time_interval <= :aggr_interval_s " +
+                                "AND g.persistence = TRUE " +
+                                "AND g.aggregation_type = 'DOUBLE' " +
+                                "ORDER BY g.time_interval"
+                        )
+                                .bind("unit_id", unitId)
+                                .bind("aggr_interval_s", maxInterval)
+                                .bind("aggr_type", AggregationType.DOUBLE)
+                                .map((rs, ctx) ->
+                                        Tuple.of(rs.getLong("group_id"), rs.getLong("sensor_id"))
+                                ).list()
+                );
+            } catch (Exception e) {
+                logger.catching(e);
+                return emptyList();
+            }
         }
 
         public long getGroupIdByUnitSensor(long unitId, long sensorId, long maxInterval) {
-            return jdbi.withHandle(h -> h.createQuery(
-                    "SELECT g.id AS group_id, g.time_interval AS time_interval FROM statistics.sensors AS s " +
-                            "JOIN statistics.sensor_to_group AS sg ON sg.sensor_id = s.id " +
-                            "JOIN statistics.groups_interval AS g ON g.id = sg.group_id " +
-                            "WHERE s.sensor_id = :sensor_id AND s.unit_id = :unit_id " +
-                            "  AND g.time_interval > 0 AND g.time_interval <= :aggr_interval_s " +
-                            "  AND g.persistence = TRUE AND g.aggregation_type = :aggr_type " +
-                            "ORDER BY g.time_interval DESC LIMIT 1"
-                    )
-                            .bind("unit_id", unitId)
-                            .bind("sensor_id", sensorId)
-                            .bind("aggr_interval_s", maxInterval)
-                            .bind("aggr_type", AggregationType.DOUBLE)
-                            .map((rs, ctx) -> rs.getLong("group_id")).first()
-            );
+            try {
+                return jdbi.<Long, IllegalStateException>withHandle(h -> h.createQuery(
+                        "SELECT g.id AS group_id, g.time_interval AS time_interval FROM statistics.sensors AS s " +
+                                "JOIN statistics.sensor_to_group AS sg ON sg.sensor_id = s.id " +
+                                "JOIN statistics.groups_interval AS g ON g.id = sg.group_id " +
+                                "WHERE s.sensor_id = :sensor_id AND s.unit_id = :unit_id " +
+                                "  AND g.time_interval > 0 AND g.time_interval <= :aggr_interval_s " +
+                                "  AND g.persistence = TRUE AND g.aggregation_type = :aggr_type " +
+                                "ORDER BY g.time_interval DESC LIMIT 1"
+                        )
+                                .bind("unit_id", unitId)
+                                .bind("sensor_id", sensorId)
+                                .bind("aggr_interval_s", maxInterval)
+                                .bind("aggr_type", AggregationType.DOUBLE)
+                                .map((rs, ctx) -> rs.getLong("group_id")).first()
+                );
+            } catch (IllegalStateException e) {
+                logger.catching(e);
+                return -1;
+            }
         }
 
     }

+ 4 - 4
src/main/java/cz/senslog/analyzer/storage/permanent/repository/StatisticsRepository.java

@@ -86,7 +86,7 @@ public class StatisticsRepository {
             }));
     }
 
-    public List<DoubleStatistics> getByTimeRange(long groupId, TimeRange<Instant> timeRange) {
+    public List<DoubleStatistics> getByTimeRange(long groupId, Tuple<Timestamp, Timestamp> timeRange) {
 
         class RawRecord {
             long recordId, groupId, sensorId, unitId;
@@ -113,11 +113,11 @@ public class StatisticsRepository {
                         "WHERE g.id = :group_id AND r.time_stamp >= :time_from " +
                         "AND (r.time_stamp + r.time_interval * interval '1 second') < :time_to " +
                         "AND r.created >= sg.created " +
-                        "ORDER BY r.created"
+                        "ORDER BY r.time_stamp ASC"
                 )
                     .bind("group_id", groupId)
-                    .bind("time_from", timeRange.getFrom())
-                    .bind("time_to", timeRange.getTo())
+                    .bind("time_from", timeRange.getItem1().toInstant())
+                    .bind("time_to", timeRange.getItem2().toInstant())
                 .map((rs, ctx) -> {
                     RawRecord r = new RawRecord();
                     r.recordId = rs.getLong("record_id");

+ 14 - 0
src/main/java/cz/senslog/analyzer/util/DateUtils.java

@@ -0,0 +1,14 @@
+package cz.senslog.analyzer.util;
+
+import java.util.Calendar;
+
+public final class DateUtils {
+
+    private DateUtils() {}
+
+    public static boolean isLeapYear(int year) {
+        Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.YEAR, year);
+        return cal.getActualMaximum(Calendar.DAY_OF_YEAR) > 365;
+    }
+}

+ 142 - 0
src/main/java/cz/senslog/analyzer/util/TimestampUtil.java

@@ -0,0 +1,142 @@
+package cz.senslog.analyzer.util;
+
+import cz.senslog.analyzer.domain.IntervalGroup;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.common.util.Tuple;
+
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static cz.senslog.analyzer.domain.Timestamp.of;
+import static cz.senslog.analyzer.util.DateUtils.isLeapYear;
+import static cz.senslog.common.util.DateTrunc.Option.*;
+import static cz.senslog.common.util.DateTrunc.trunc;
+
+public final class TimestampUtil {
+    // 28 = 2_419_200
+    // 29 = 2_505_600
+    // 30 = 2_592_000
+    // 31 = 2_678_400
+
+    private static final int [] MONTHS_BY_SECONDS = new int[] {
+            2_678_400,  /* january  */  2_505_600, /* february  */
+            2_678_400,  /* march    */  2_592_000, /* april     */
+            2_678_400,  /* may      */  2_592_000, /* june      */
+            2_678_400,  /* july     */  2_678_400, /* august    */
+            2_592_000,  /* september*/  2_678_400, /* october   */
+            2_592_000,  /* november */  2_678_400, /* december  */
+    };
+
+    private static final Map<IntervalGroup, Function<Tuple<Timestamp, Timestamp>, Integer>> _GROUP_TO_INTERVAL;
+    private static final Function<Tuple<Timestamp, Timestamp>, Integer> DEFAULT_FNC = tr -> -1;
+
+    static {
+        _GROUP_TO_INTERVAL = new HashMap<>(IntervalGroup.values().length);
+        _GROUP_TO_INTERVAL.put(IntervalGroup.HOUR, tr -> 3_600);
+        _GROUP_TO_INTERVAL.put(IntervalGroup.DAY, tr -> 86_400);
+        _GROUP_TO_INTERVAL.put(IntervalGroup.MONTH, tr -> {
+            int yearFrom = tr.getItem1().get().getYear();
+            int yearTo = tr.getItem2().get().getYear();
+            int monthFrom = tr.getItem1().get().getMonthValue();
+            int monthTo = tr.getItem2().get().getMonthValue();
+
+            int yearDiff = yearTo - yearFrom;
+
+            // over two years
+            if (yearDiff > 1) {
+                int februarySec = MONTHS_BY_SECONDS[1];
+                for (int year = yearFrom+1; year < yearTo; year++) {
+                    if (isLeapYear(year)) {
+                        return februarySec - 86_400; // 29 - 1 day
+                    }
+                }
+                if (monthFrom <= 2 && isLeapYear(yearFrom)) {
+                    return februarySec - 86_400; // 29 - 1 day
+                }
+
+                if (monthTo >= 2 && isLeapYear(yearTo)) {
+                    return februarySec - 86_400; // 29 - 1 day
+                }
+
+                return februarySec;
+            }
+
+            // over a year
+            if (yearDiff == 1) {
+                int minMonthIdx = monthFrom - 1;
+                for (int monthIdx = monthFrom; monthIdx < 12; monthIdx++) {
+                    if (MONTHS_BY_SECONDS[monthIdx] < MONTHS_BY_SECONDS[minMonthIdx]) {
+                        minMonthIdx = monthIdx;
+                    }
+                }
+                int minIntervalFirst = MONTHS_BY_SECONDS[minMonthIdx];
+                minIntervalFirst = minMonthIdx == 1 && isLeapYear(yearFrom) ? minIntervalFirst-86_400 : minIntervalFirst;
+                minMonthIdx = 0;
+
+                for (int month = 1; month < monthTo; month++) {
+                    if (MONTHS_BY_SECONDS[month] < MONTHS_BY_SECONDS[minMonthIdx]) {
+                        minMonthIdx = month;
+                    }
+                }
+                int minIntervalSecond = MONTHS_BY_SECONDS[minMonthIdx];
+                minIntervalSecond = minMonthIdx == 1 && isLeapYear(yearTo) ? minIntervalSecond-86_400 : minIntervalSecond;
+                return Math.min(minIntervalFirst, minIntervalSecond);
+            }
+
+            // the same year
+            boolean leapYear = isLeapYear(yearFrom);
+            if (2 >= monthFrom && 2 <= monthTo) {
+                int februarySec = MONTHS_BY_SECONDS[1];
+                return leapYear ? februarySec - 86_400 : februarySec;
+            }
+
+            int minInterval = MONTHS_BY_SECONDS[monthFrom - 1];
+            for (int i = monthFrom; i < monthTo; i++) {
+                if (MONTHS_BY_SECONDS[i] < minInterval) {
+                    minInterval = MONTHS_BY_SECONDS[i];
+                }
+            }
+            return minInterval;
+        });
+        _GROUP_TO_INTERVAL.put(IntervalGroup.YEAR, tr -> 31_556_926);
+    }
+
+    private TimestampUtil() {}
+
+    public static Optional<Timestamp> parseTimestamp(String value) {
+        if (value == null) { return Optional.empty(); }
+        try {
+            return Optional.of(Timestamp.parse(value));
+        } catch (NumberFormatException e) {
+            return Optional.empty();
+        }
+    }
+
+    public static int differenceByIntervalGroup(Tuple<Timestamp, Timestamp> timeRange, IntervalGroup intervalGroup) {
+        if (timeRange == null || intervalGroup == null) { return -1; }
+        return _GROUP_TO_INTERVAL.getOrDefault(intervalGroup, DEFAULT_FNC).apply(timeRange);
+    }
+
+    private static Timestamp truncByGroup(Timestamp timestamp, IntervalGroup intervalGroup, int addition) {
+        switch (intervalGroup) {
+            case HOUR:  return of(trunc(timestamp.get(), HOUR).plusHours(addition));
+            case DAY:   return of(trunc(timestamp.get(), DAY).plusDays(addition));
+            case MONTH: return of(trunc(timestamp.get(), MONTH).plusMonths(addition));
+            case YEAR:  return of(trunc(timestamp.get(), YEAR).plusYears(addition));
+            default:    return timestamp;
+        }
+    }
+
+    public static Timestamp truncByGroup(Timestamp timestamp, IntervalGroup intervalGroup) {
+        return truncByGroup(timestamp, intervalGroup, 0);
+    }
+
+    public static Tuple<Timestamp, Timestamp> truncToIntervalByGroup(Timestamp from, Timestamp to, IntervalGroup intervalGroup) {
+        Timestamp fromTrunc = truncByGroup(from, intervalGroup, 1);
+        Timestamp toTrunc = truncByGroup(to, intervalGroup).minus(1, ChronoUnit.SECONDS);
+        return Tuple.of(fromTrunc, toTrunc);
+    }
+}

+ 1 - 1
src/main/java/cz/senslog/analyzer/server/Server.java → src/main/java/cz/senslog/analyzer/ws/Server.java

@@ -1,4 +1,4 @@
-package cz.senslog.analyzer.server;
+package cz.senslog.analyzer.ws;
 
 public interface Server {
 

+ 1 - 1
src/main/java/cz/senslog/analyzer/server/ServerComponent.java → src/main/java/cz/senslog/analyzer/ws/ServerComponent.java

@@ -1,4 +1,4 @@
-package cz.senslog.analyzer.server;
+package cz.senslog.analyzer.ws;
 
 import dagger.Component;
 

+ 3 - 3
src/main/java/cz/senslog/analyzer/server/ServerModule.java → src/main/java/cz/senslog/analyzer/ws/ServerModule.java

@@ -1,7 +1,7 @@
-package cz.senslog.analyzer.server;
+package cz.senslog.analyzer.ws;
 
-import cz.senslog.analyzer.server.vertx.VertxHandlersModule;
-import cz.senslog.analyzer.server.vertx.VertxServer;
+import cz.senslog.analyzer.ws.vertx.VertxHandlersModule;
+import cz.senslog.analyzer.ws.vertx.VertxServer;
 import dagger.Binds;
 import dagger.Module;
 

+ 94 - 0
src/main/java/cz/senslog/analyzer/ws/dto/SensorStatisticsData.java

@@ -0,0 +1,94 @@
+package cz.senslog.analyzer.ws.dto;
+
+import cz.senslog.analyzer.domain.DoubleStatistics;
+import cz.senslog.analyzer.domain.Timestamp;
+
+import java.util.List;
+
+public class SensorStatisticsData {
+
+    public static class Data {
+        private final double min, max, avg, sum;
+        private final Timestamp timestamp;
+
+        public Data(double min, double max, double avg, double sum, Timestamp timestamp) {
+            this.min = min;
+            this.max = max;
+            this.avg = avg;
+            this.sum = sum;
+            this.timestamp = timestamp;
+        }
+
+        public Data(DoubleStatistics st) {
+            this(st.getMin(), st.getMax(), st.getAverage(), st.getSum(), st.getTimestamp());
+        }
+
+        public double getMin() {
+            return min;
+        }
+
+        public double getMax() {
+            return max;
+        }
+
+        public double getAvg() {
+            return avg;
+        }
+
+        public double getSum() {
+            return sum;
+        }
+
+        public Timestamp getTimestamp() {
+            return timestamp;
+        }
+
+        @Override
+        public String toString() {
+            return "{" +
+                    "min=" + min +
+                    ", max=" + max +
+                    ", avg=" + avg +
+                    ", sum=" + sum +
+                    ", timestamp=" + timestamp +
+                    '}';
+        }
+    }
+
+    private final long sensorId;
+    private final long interval;
+    private final List<Data> data;
+    private final Data statistics;
+
+    public SensorStatisticsData(long sensorId, long interval, List<Data> data, Data statistics) {
+        this.sensorId = sensorId;
+        this.interval = interval;
+        this.data = data;
+        this.statistics = statistics;
+    }
+
+    public long getSensorId() {
+        return sensorId;
+    }
+
+    public long getInterval() {
+        return interval;
+    }
+
+    public List<Data> getData() {
+        return data;
+    }
+
+    public Data getStatistics() {
+        return statistics;
+    }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "interval=" + interval +
+                ", data=" + data +
+                ", statistics=" + statistics +
+                '}';
+    }
+}

+ 2 - 2
src/main/java/cz/senslog/analyzer/server/handler/GroupsHandler.java → src/main/java/cz/senslog/analyzer/ws/handler/GroupsHandler.java

@@ -1,7 +1,7 @@
-package cz.senslog.analyzer.server.handler;
+package cz.senslog.analyzer.ws.handler;
 
 import cz.senslog.analyzer.storage.permanent.repository.StatisticsConfigRepository;
-import cz.senslog.analyzer.server.vertx.AbstractRestHandler;
+import cz.senslog.analyzer.ws.vertx.AbstractRestHandler;
 
 import javax.inject.Inject;
 

+ 2 - 4
src/main/java/cz/senslog/analyzer/server/handler/InfoHandler.java → src/main/java/cz/senslog/analyzer/ws/handler/InfoHandler.java

@@ -1,11 +1,9 @@
-package cz.senslog.analyzer.server.handler;
+package cz.senslog.analyzer.ws.handler;
 
 import com.google.gson.JsonObject;
 import cz.senslog.analyzer.app.Application;
-import cz.senslog.analyzer.server.vertx.AbstractRestHandler;
+import cz.senslog.analyzer.ws.vertx.AbstractRestHandler;
 import io.vertx.core.http.HttpServerResponse;
-import io.vertx.ext.web.Route;
-import io.vertx.ext.web.RoutingContext;
 
 import javax.inject.Inject;
 

+ 121 - 0
src/main/java/cz/senslog/analyzer/ws/handler/StatisticsHandler.java

@@ -0,0 +1,121 @@
+package cz.senslog.analyzer.ws.handler;
+
+import cz.senslog.analyzer.domain.*;
+import cz.senslog.analyzer.ws.dto.SensorStatisticsData;
+import cz.senslog.analyzer.ws.manager.WSStatisticsManager;
+import cz.senslog.analyzer.ws.vertx.AbstractRestHandler;
+import io.vertx.core.MultiMap;
+import io.vertx.core.http.HttpServerResponse;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+import java.util.*;
+
+import static cz.senslog.analyzer.domain.IntervalGroup.parseIntervalGroup;
+import static cz.senslog.analyzer.util.TimestampUtil.parseTimestamp;
+import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
+import static cz.senslog.common.util.number.LongUtils.parseLong;
+
+public class StatisticsHandler extends AbstractRestHandler {
+
+    private static final Logger logger = LogManager.getLogger(StatisticsHandler.class);
+
+    private final WSStatisticsManager manager;
+
+    @Inject
+    public StatisticsHandler(WSStatisticsManager manager) {
+        this.manager = manager;
+    }
+
+    @Override
+    public void start() {
+
+        router().get("/analytics").handler(ctx -> {
+            logger.info("Handling '{}' with the params '{}'.", ctx.request().path(), ctx.request().params().entries());
+
+            HttpServerResponse response = ctx.response();
+            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            MultiMap params = ctx.request().params();
+
+            Optional<Long> unitIdOpt = parseLong(params.get("unit_id"));
+            if (!unitIdOpt.isPresent()) {
+                ctx.fail(400, new Throwable(
+                        "Parameter 'unit_id' is not at the correct format. Expected long number."
+                )); return;
+            }
+
+            Optional<Long> sensorIdOpt = parseLong(params.get("sensor_id"));
+
+            Optional<IntervalGroup> intervalGroupOpt = Optional.ofNullable(parseIntervalGroup(params.get("interval")));
+            if (!intervalGroupOpt.isPresent()) {
+                ctx.fail(400, new Throwable(
+                        "Parameter 'group_by' is not at the correct format. " +
+                        "Expected one of these values: "+ Arrays.toString(IntervalGroup.values()) +"."
+                )); return;
+            }
+
+            Optional<Timestamp> fromOpt = parseTimestamp(params.get("from"));
+            if (!fromOpt.isPresent()) {
+                Timestamp now = Timestamp.now();
+                ctx.fail(400, new Throwable(
+                        "Parameter 'from' is not at the correct format. " +
+                        "Expected a date at the format 'yyyy-MM-dd HH:mm:ss+HH' or 'yyyy-MM-dd'. " +
+                        "For example: '"+now.format()+"' or '"+now.dateFormat()+"'."
+                )); return;
+            }
+
+            Optional<Timestamp> toOpt = parseTimestamp(params.get("to"));
+            if (!toOpt.isPresent()) {
+                Timestamp now = Timestamp.now();
+                ctx.fail(400, new Throwable(
+                        "Parameter 'to' is not at the correct format. " +
+                        "Expected a date at the format 'yyyy-MM-dd HH:mm:ss+HH' or 'yyyy-MM-dd'. " +
+                        "For example: '"+now.format()+"' or '"+now.dateFormat()+"'."
+                )); return;
+            }
+
+            List<SensorStatisticsData> statisticsData;
+            if (sensorIdOpt.isPresent()) {
+                statisticsData = manager.loadData(unitIdOpt.get(), sensorIdOpt.get(), fromOpt.get(), toOpt.get(), intervalGroupOpt.get());
+            } else {
+                statisticsData = manager.loadData(unitIdOpt.get(), fromOpt.get(), toOpt.get(), intervalGroupOpt.get());
+            }
+
+            if (statisticsData.isEmpty()) {
+                ctx.fail(204, new Throwable(
+                        "No data loaded according to the input parameters."
+                )); return;
+            }
+
+            JsonObject jsonSensors = new JsonObject();
+            for (SensorStatisticsData data : statisticsData) {
+                JsonObject jsonObject = new JsonObject();
+                jsonObject.put("interval", data.getInterval());
+                JsonArray dataArray = new JsonArray();
+                for (SensorStatisticsData.Data stData : data.getData()) {
+                    dataArray.add(new JsonObject()
+                            .put("min", stData.getMin())
+                            .put("max", stData.getMax())
+                            .put("avg", stData.getAvg())
+                            .put("sum", stData.getSum())
+                            .put("timestamp", stData.getTimestamp().format())
+                    );
+                }
+                jsonObject.put("data", dataArray);
+                jsonObject.put("statistics", new JsonObject()
+                        .put("min", data.getStatistics().getMin())
+                        .put("max", data.getStatistics().getMax())
+                        .put("avg", data.getStatistics().getAvg())
+                        .put("sum", data.getStatistics().getSum())
+                );
+                jsonSensors.put(String.valueOf(data.getSensorId()), jsonObject);
+            }
+
+            response.end(jsonSensors.encode());
+        });
+    }
+}

+ 110 - 0
src/main/java/cz/senslog/analyzer/ws/manager/WSStatisticsManager.java

@@ -0,0 +1,110 @@
+package cz.senslog.analyzer.ws.manager;
+
+import cz.senslog.analyzer.domain.DoubleStatistics;
+import cz.senslog.analyzer.domain.Group;
+import cz.senslog.analyzer.domain.IntervalGroup;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.analyzer.provider.ScheduledDatabaseProvider;
+import cz.senslog.analyzer.storage.permanent.repository.StatisticsConfigRepository;
+import cz.senslog.analyzer.storage.permanent.repository.StatisticsRepository;
+import cz.senslog.analyzer.util.TimestampUtil;
+import cz.senslog.analyzer.ws.dto.SensorStatisticsData;
+import cz.senslog.common.util.Tuple;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+import java.util.*;
+
+import static cz.senslog.analyzer.domain.DoubleStatistics.init;
+import static cz.senslog.analyzer.util.TimestampUtil.truncByGroup;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+public class WSStatisticsManager {
+
+    private static final Logger logger = LogManager.getLogger(ScheduledDatabaseProvider.class);
+
+    private final StatisticsRepository statisticsRepository;
+    private final StatisticsConfigRepository configRepository;
+
+    @Inject
+    public WSStatisticsManager(StatisticsRepository statisticsRepository, StatisticsConfigRepository configRepository) {
+        this.statisticsRepository = statisticsRepository;
+        this.configRepository = configRepository;
+    }
+
+    private static Tuple<Timestamp, Timestamp> createIntervalByIntervalGroup(Timestamp from, Timestamp to, IntervalGroup intervalGroup) {
+        Tuple<Timestamp, Timestamp> interval = TimestampUtil.truncToIntervalByGroup(from, to, intervalGroup);
+
+        if (interval.getItem1().isAfter(interval.getItem2())) {
+            throw new IllegalArgumentException("The input interval is too short according to the configuration. " +
+                    "The 'from' parameter was truncated to '"+interval.getItem1().format()+"' and " +
+                    "the 'to' parameter was truncated to '"+interval.getItem2().format()+"'.");
+        }
+
+        return interval;
+    }
+
+    private static Tuple<DoubleStatistics, Collection<DoubleStatistics>> aggregateStatistics(List<DoubleStatistics> statistics, IntervalGroup intervalGroup) {
+
+        if (statistics == null || statistics.isEmpty()) {
+            return null;
+        }
+
+        DoubleStatistics firstSt = statistics.get(0);
+        Group group = firstSt.getSource();
+        DoubleStatistics aggrSt = init(firstSt.getSource(), firstSt.getTimestamp());
+        Map<Timestamp, DoubleStatistics> intervalsSt = new HashMap<>();
+        for (DoubleStatistics st : statistics) {
+            aggrSt.accept(st);
+            Timestamp tmTrunc = truncByGroup(st.getTimestamp(), intervalGroup);
+            intervalsSt.computeIfAbsent(tmTrunc, tm -> init(group, tm)).accept(st);
+        }
+
+        return Tuple.of(aggrSt, intervalsSt.values());
+    }
+
+    private static List<SensorStatisticsData> mapToStatisticsData(long sensorId, long intervalSec, Tuple<DoubleStatistics, Collection<DoubleStatistics>> statistics) {
+        if (statistics == null) { return emptyList(); }
+
+        DoubleStatistics sumSt = statistics.getItem1();
+        Collection<DoubleStatistics> aggrSt = statistics.getItem2();
+        List<SensorStatisticsData.Data> statisticsData = new ArrayList<>(aggrSt.size());
+        for (DoubleStatistics st : aggrSt) {
+            statisticsData.add(new SensorStatisticsData.Data(st));
+        }
+
+        statisticsData.sort(Comparator.comparing(SensorStatisticsData.Data::getTimestamp));
+        SensorStatisticsData.Data aggregated = new SensorStatisticsData.Data(sumSt);
+        return singletonList(new SensorStatisticsData(sensorId, intervalSec, statisticsData, aggregated));
+    }
+
+    public List<SensorStatisticsData> loadData(long unitId, long sensorId, Timestamp from, Timestamp to, IntervalGroup intervalGroup) {
+        Tuple<Timestamp, Timestamp> timeRange = createIntervalByIntervalGroup(from, to, intervalGroup);
+        long intervalSec = TimestampUtil.differenceByIntervalGroup(timeRange, intervalGroup);
+
+        long groupId = configRepository.special().getGroupIdByUnitSensor(unitId, sensorId, intervalSec);
+        if (groupId <= 0) {
+            throw new IllegalArgumentException(String.format(
+                    "None group for the combination of unit_id '%d' and sensor_id '%d' and interval %d seconds.", unitId, sensorId, intervalSec
+            ));
+        }
+
+        List<DoubleStatistics> statistics = statisticsRepository.getByTimeRange(groupId, timeRange);
+        return mapToStatisticsData(sensorId, intervalSec, aggregateStatistics(statistics, intervalGroup));
+    }
+
+    public List<SensorStatisticsData> loadData(long unitId, Timestamp from, Timestamp to, IntervalGroup intervalGroup) {
+        Tuple<Timestamp, Timestamp> timeRange = createIntervalByIntervalGroup(from, to, intervalGroup);
+        long intervalSec = TimestampUtil.differenceByIntervalGroup(timeRange, intervalGroup);
+        List<Tuple<Long, Long>> groupsBySensor = configRepository.special().getGroupsByUnit(unitId, intervalSec);
+        List<SensorStatisticsData> statisticsDataList = new ArrayList<>(groupsBySensor.size());
+        for (Tuple<Long, Long> tuple : groupsBySensor) {
+            long groupId = tuple.getItem1(); long sensorId = tuple.getItem2();
+            List<DoubleStatistics> statistics = statisticsRepository.getByTimeRange(groupId, timeRange);
+            statisticsDataList.addAll(mapToStatisticsData(sensorId, intervalSec, aggregateStatistics(statistics, intervalGroup)));
+        }
+        return statisticsDataList;
+    }
+}

+ 1 - 1
src/main/java/cz/senslog/analyzer/server/vertx/AbstractRestHandler.java → src/main/java/cz/senslog/analyzer/ws/vertx/AbstractRestHandler.java

@@ -1,4 +1,4 @@
-package cz.senslog.analyzer.server.vertx;
+package cz.senslog.analyzer.ws.vertx;
 
 import io.vertx.core.Vertx;
 import io.vertx.ext.web.Router;

+ 4 - 4
src/main/java/cz/senslog/analyzer/server/vertx/VertxHandlersModule.java → src/main/java/cz/senslog/analyzer/ws/vertx/VertxHandlersModule.java

@@ -1,9 +1,9 @@
-package cz.senslog.analyzer.server.vertx;
+package cz.senslog.analyzer.ws.vertx;
 
 import cz.senslog.analyzer.storage.RepositoryModule;
-import cz.senslog.analyzer.server.handler.GroupsHandler;
-import cz.senslog.analyzer.server.handler.InfoHandler;
-import cz.senslog.analyzer.server.handler.StatisticsHandler;
+import cz.senslog.analyzer.ws.handler.GroupsHandler;
+import cz.senslog.analyzer.ws.handler.InfoHandler;
+import cz.senslog.analyzer.ws.handler.StatisticsHandler;
 import dagger.Binds;
 import dagger.Module;
 

+ 21 - 5
src/main/java/cz/senslog/analyzer/server/vertx/VertxServer.java → src/main/java/cz/senslog/analyzer/ws/vertx/VertxServer.java

@@ -1,10 +1,11 @@
-package cz.senslog.analyzer.server.vertx;
+package cz.senslog.analyzer.ws.vertx;
 
-import cz.senslog.analyzer.server.Server;
-import cz.senslog.analyzer.server.handler.GroupsHandler;
-import cz.senslog.analyzer.server.handler.InfoHandler;
-import cz.senslog.analyzer.server.handler.StatisticsHandler;
+import cz.senslog.analyzer.ws.Server;
+import cz.senslog.analyzer.ws.handler.GroupsHandler;
+import cz.senslog.analyzer.ws.handler.InfoHandler;
+import cz.senslog.analyzer.ws.handler.StatisticsHandler;
 import io.vertx.core.*;
+import io.vertx.core.http.HttpServerResponse;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.Router;
 import org.apache.logging.log4j.LogManager;
@@ -14,6 +15,9 @@ import javax.inject.Inject;
 import java.util.HashMap;
 import java.util.Map;
 
+import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
+
 
 public class VertxServer extends AbstractVerticle implements Server {
 
@@ -47,6 +51,18 @@ public class VertxServer extends AbstractVerticle implements Server {
             router.mountSubRouter(handlerEntry.getKey(), handlerEntry.getValue().start(vertx));
         }
 
+        router.route().failureHandler(ctx -> {
+            logger.catching(ctx.failure());
+            HttpServerResponse response = ctx.response();
+            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            JsonObject error = new JsonObject()
+                    .put("timestamp", System.nanoTime())
+                    .put("message", ctx.failure().getMessage())
+                    .put("path", ctx.request().path());
+            int code = ctx.statusCode() > 0 ? ctx.statusCode() : 400;
+            response.setStatusCode(code).end(error.encode());
+        });
+
         vertx.createHttpServer()
                 .requestHandler(router)
                 .listen(config().getInteger("http.server.port"), result -> {

+ 123 - 0
src/test/java/cz/senslog/analyzer/util/TimestampUtilTest.java

@@ -0,0 +1,123 @@
+package cz.senslog.analyzer.util;
+
+import cz.senslog.analyzer.domain.IntervalGroup;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.common.util.TimeRange;
+import cz.senslog.common.util.Tuple;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+
+import static cz.senslog.analyzer.domain.Timestamp.of;
+import static java.time.ZoneOffset.UTC;
+import static org.junit.jupiter.api.Assertions.*;
+
+class TimestampUtilTest {
+
+    @Test
+    void truncToIntervalByGroup_HOUR_equals_true() {
+
+        LocalDateTime fromLocal = LocalDateTime.of(2020, 1, 30, 14, 30, 15);
+        LocalDateTime toLocal = LocalDateTime.of(2020, 1, 30, 15, 30, 15);
+
+        Timestamp from = of(OffsetDateTime.of(fromLocal, ZoneOffset.UTC));
+        Timestamp to = of(OffsetDateTime.of(toLocal, ZoneOffset.UTC));
+
+        Tuple<Timestamp, Timestamp> interval = TimestampUtil.truncToIntervalByGroup(from, to, IntervalGroup.HOUR);
+
+        assertNull(interval);
+    }
+
+    @Test
+    void diff_1_MONTH_true() {
+        LocalDateTime from = LocalDateTime.of(2020, 1, 1, 0, 0);
+        LocalDateTime to = from.plusMonths(1).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_678_400, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_5_MONTH_true() {
+        LocalDateTime from = LocalDateTime.of(2020, 3, 1, 0, 0);
+        LocalDateTime to = from.plusMonths(10).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_592_000, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_10_MONTH_leapYear_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 1, 1, 0, 0);
+        LocalDateTime to = from.plusMonths(10).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_419_200, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_15_MONTH_leapYear_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 1, 1, 0, 0);
+        LocalDateTime to = from.plusMonths(15).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_419_200, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_15_MONTH_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 3, 1, 0, 0);
+        LocalDateTime to = from.plusMonths(15).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_505_600, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_2_YEARS_2_MONTHS_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 12, 1, 0, 0);
+        LocalDateTime to = LocalDateTime.of(2001, 2, 1, 0, 0).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_678_400, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_3_YEARS_noLeapYear_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 12, 1, 0, 0);
+        LocalDateTime to = LocalDateTime.of(2003, 2, 1, 0, 0).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_505_600, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_4_YEARS_LastYEAR_leapYear_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 12, 1, 0, 0);
+        LocalDateTime to = LocalDateTime.of(2004, 5, 1, 0, 0).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_419_200, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_3_YEARS_FirstYEAR_leapYear_true() {
+        LocalDateTime from = LocalDateTime.of(2000, 1, 1, 0, 0);
+        LocalDateTime to = from.plusYears(3).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_419_200, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+
+    @Test
+    void diff_3_YEARS_MiddleYEAR_leapYear_true() {
+        LocalDateTime from = LocalDateTime.of(1999, 12, 1, 0, 0);
+        LocalDateTime to = from.plusYears(3).minusSeconds(1);
+        Tuple<Timestamp, Timestamp> timeRange = Tuple.of(of(OffsetDateTime.of(from, UTC)), of(OffsetDateTime.of(to, UTC)));
+
+        assertEquals(2_419_200, TimestampUtil.differenceByIntervalGroup(timeRange, IntervalGroup.MONTH));
+    }
+}

+ 102 - 0
src/test/java/cz/senslog/analyzer/ws/manager/WSStatisticsManagerTest.java

@@ -0,0 +1,102 @@
+package cz.senslog.analyzer.ws.manager;
+
+import cz.senslog.analyzer.domain.*;
+import cz.senslog.analyzer.storage.permanent.repository.StatisticsConfigRepository;
+import cz.senslog.analyzer.storage.permanent.repository.StatisticsRepository;
+import cz.senslog.analyzer.ws.dto.SensorStatisticsData;
+import cz.senslog.common.util.Tuple;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+import static cz.senslog.analyzer.domain.AggregationType.DOUBLE;
+import static java.time.temporal.ChronoUnit.*;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class WSStatisticsManagerTest {
+
+    @BeforeEach
+    void setUp() {
+
+
+    }
+
+    private static WSStatisticsManager createManager(Group group, List<DoubleStatistics> doubleStatistics) {
+        StatisticsConfigRepository.Special specialRepo = mock(StatisticsConfigRepository.Special.class);
+        when(specialRepo.getGroupIdByUnitSensor(anyLong(), anyLong(), anyLong())).thenReturn(group.getId());
+        StatisticsConfigRepository configRepo = mock(StatisticsConfigRepository.class);
+        when(configRepo.special()).thenReturn(specialRepo);
+
+        StatisticsRepository statisticsRepo = mock(StatisticsRepository.class);
+        when(statisticsRepo.getByTimeRange(anyLong(), any(Tuple.class))).thenReturn(doubleStatistics);
+        return new WSStatisticsManager(statisticsRepo, configRepo);
+    }
+
+
+    @Test
+    void loadData() {
+
+        long unitId = 0;
+        long sensorId = 0;
+
+        Group group = new Group(0, 3600, true, DOUBLE,
+                new HashSet<>(singletonList(new Sensor(unitId, sensorId)))
+        );
+
+        LocalDateTime startDate = LocalDateTime.of(2020, 1, 1, 1, 1);
+        Timestamp from = Timestamp.of(OffsetDateTime.of(startDate, ZoneOffset.UTC));
+        Timestamp to = from.plus(1, ChronoUnit.MONTHS);
+        IntervalGroup intervalGroup = IntervalGroup.DAY;
+
+        WSStatisticsManager manager = createManager(group, Arrays.asList(
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(1, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(1, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(2, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(2, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(3, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(3, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(4, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(4, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(5, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(5, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(10, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(10, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(11, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(11, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(12, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(12, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(13, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(13, DAYS).plus(22, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(14, DAYS).plus(1, HOURS)),
+                new DoubleStatistics(group, 2, 10, 20, 30, from.plus(14, DAYS).plus(22, HOURS))
+            )
+        );
+
+        List<SensorStatisticsData> dataList = manager.loadData(unitId, sensorId, from, to, intervalGroup);
+
+        assertEquals(1, dataList.size());
+
+        SensorStatisticsData sensorData = dataList.get(0);
+        assertEquals(10, sensorData.getData().size());
+
+        SensorStatisticsData.Data statisticsAggr = sensorData.getStatistics();
+        assertEquals(600, statisticsAggr.getSum());
+        assertEquals(10, statisticsAggr.getMin());
+        assertEquals(20, statisticsAggr.getMax());
+        assertEquals(15, statisticsAggr.getAvg());
+    }
+}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác