Lukas Cerny 5 gadi atpakaļ
vecāks
revīzija
93d6bf5726

+ 6 - 2
src/main/java/cz/senslog/analyzer/domain/DoubleStatistics.java

@@ -97,8 +97,12 @@ public class DoubleStatistics extends Data<Group, DoubleStatistics> {
         return getSource().getSensors().contains(sensor);
     }
 
-    public boolean accept(Sensor sensor, DoubleStatistics other) {
-        if (acceptSensor(sensor)) {
+    private boolean acceptGroup(Group group) {
+        return getSource().getId() == group.getId();
+    }
+
+    public boolean accept(DoubleStatistics other) {
+        if (acceptGroup(other.getSource())) {
             this.count += other.count;
             this.simpleSum += other.simpleSum;
             this.sumWithCompensation(other.sum);

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

@@ -4,6 +4,6 @@ public enum  GroupBy {
     DAY, MONTH, YEAR;
 
     public static GroupBy parse(String groupBy) {
-        return valueOf(groupBy);
+        return groupBy != null ? valueOf(groupBy) : null;
     }
 }

+ 8 - 6
src/main/java/cz/senslog/analyzer/domain/Interval.java

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

+ 60 - 4
src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsConfigRepository.java

@@ -1,29 +1,35 @@
 package cz.senslog.analyzer.persistence.repository;
 
-import cz.senslog.analyzer.domain.AggregationType;
-import cz.senslog.analyzer.domain.Group;
-import cz.senslog.analyzer.domain.Sensor;
-import cz.senslog.analyzer.domain.Threshold;
+import cz.senslog.analyzer.domain.*;
 import cz.senslog.analyzer.persistence.Connection;
+import cz.senslog.common.util.Tuple;
 import org.jdbi.v3.core.Jdbi;
 
 import javax.inject.Inject;
 import java.util.AbstractMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 
 import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
 import static java.util.stream.Collectors.*;
 
 public class StatisticsConfigRepository {
 
     private final Jdbi jdbi;
 
+    private final Special specialRepository;
+
     @Inject
     public StatisticsConfigRepository(Connection<Jdbi> connection) {
         this.jdbi = connection.get();
+        this.specialRepository = new Special(connection.get());
     }
 
+    public Special special() {
+        return specialRepository;
+    }
 
     public List<Sensor> getAllAvailableSensors() {
         return jdbi.withHandle(h -> h.createQuery(
@@ -105,4 +111,54 @@ public class StatisticsConfigRepository {
                         )).list()
         );
     }
+
+    public static class Special {
+
+        private final Jdbi jdbi;
+
+        private Special(Jdbi jdbi) {
+            this.jdbi = jdbi;
+        }
+
+        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()
+            );
+        }
+
+        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()
+            );
+        }
+
+    }
 }

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

@@ -110,7 +110,7 @@ public class StatisticsRepository {
                         "JOIN statistics.groups_interval AS g ON g.id = r.group_id " +
                         "JOIN statistics.sensor_to_group AS sg ON sg.group_id = r.group_id " +
                         "JOIN statistics.sensors AS s ON s.id = sg.sensor_id " +
-                        "WHERE r.time_stamp >= :time_from " +
+                        "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"

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

@@ -1,17 +0,0 @@
-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.");
-    }
-}

+ 21 - 0
src/main/java/cz/senslog/analyzer/server/handler/GroupsHandler.java

@@ -0,0 +1,21 @@
+package cz.senslog.analyzer.server.handler;
+
+import cz.senslog.analyzer.persistence.repository.StatisticsConfigRepository;
+import cz.senslog.analyzer.server.vertx.AbstractRestHandler;
+
+import javax.inject.Inject;
+
+public class GroupsHandler extends AbstractRestHandler {
+
+    private final StatisticsConfigRepository repository;
+
+    @Inject
+    public GroupsHandler(StatisticsConfigRepository repository) {
+        this.repository = repository;
+    }
+
+    @Override
+    public void start() {
+
+    }
+}

+ 15 - 27
src/main/java/cz/senslog/analyzer/server/handler/InfoHandler.java

@@ -2,8 +2,9 @@ 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 cz.senslog.analyzer.server.vertx.AbstractRestHandler;
 import io.vertx.core.http.HttpServerResponse;
+import io.vertx.ext.web.Route;
 import io.vertx.ext.web.RoutingContext;
 
 import javax.inject.Inject;
@@ -12,40 +13,27 @@ 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 {
+public class InfoHandler extends AbstractRestHandler {
 
     @Inject
     public InfoHandler(){}
 
     @Override
-    public void exception(RoutingContext context) {
-        HttpServerResponse response = context.response();
+    public void start() {
 
-        response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
+        router().get().handler(ctx -> {
+            HttpServerResponse response = ctx.response();
+            response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
 
-        JsonObject json = new JsonObject();
+            JsonObject uptime = new JsonObject();
+            uptime.addProperty("app", Application.uptime() / 1000);
+            uptime.addProperty("jvm", Application.uptimeJVM() / 1000);
 
-        json.addProperty("status", "failed");
-        json.addProperty("message", context.failure().getMessage());
-        json.addProperty("code", 400);
+            JsonObject json = new JsonObject();
+            json.addProperty("status", "ok");
+            json.add("uptime", uptime);
 
-        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));
+            response.end(objectToJson(json));
+        });
     }
 }

+ 158 - 26
src/main/java/cz/senslog/analyzer/server/handler/StatisticsHandler.java

@@ -2,33 +2,42 @@ 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.StatisticsConfigRepository;
 import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
-import cz.senslog.analyzer.server.vertx.VertxAbstractHandler;
+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.ext.web.RoutingContext;
+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.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 VertxAbstractHandler {
+public class StatisticsHandler extends AbstractRestHandler {
 
-    private final StatisticsRepository repository;
+    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) -> {
-                JsonObject js = new JsonObject();
+                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());
@@ -41,33 +50,145 @@ public class StatisticsHandler extends VertxAbstractHandler {
             .create();
 
     @Inject
-    public StatisticsHandler(StatisticsRepository repository) {
-        this.repository = repository;
+    public StatisticsHandler(
+            StatisticsRepository statisticsRepository,
+            StatisticsConfigRepository configRepository
+    ) {
+        this.statisticsRepository = statisticsRepository;
+        this.configRepository = configRepository;
     }
 
-
     @Override
-    public void get(RoutingContext context) throws RuntimeException {
-        HttpServerResponse response = context.response();
-        MultiMap params = context.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 = params.contains("groupBy") ? GroupBy.parse(params.get("groupBy")) : null;
-        TimeRange<Instant> timeRange = TimeRange.of(from.toInstant(), to.toInstant());
-
-        Object result;
-        List<DoubleStatistics> statistics = repository.getByTimeRange(groupId, timeRange);
-        result = groupBy == null ? statistics : MergeStatistics.merge(groupBy, statistics);
-
-        response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
-        response.end(gson.toJson(result));
+    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 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>> merge(GroupBy groupBy, List<DoubleStatistics> statistics) {
+        public static Map<String, List<DoubleStatistics>> mergeByGroup(GroupBy groupBy, List<DoubleStatistics> statistics) {
             Map<String, List<DoubleStatistics>> result = new HashMap<>();
             DateTimeFormatter formatter = null;
 
@@ -84,5 +205,16 @@ public class StatisticsHandler extends VertxAbstractHandler {
 
             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;
+        }
     }
 }

+ 21 - 0
src/main/java/cz/senslog/analyzer/server/vertx/AbstractRestHandler.java

@@ -0,0 +1,21 @@
+package cz.senslog.analyzer.server.vertx;
+
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.Router;
+
+public abstract class AbstractRestHandler {
+
+    private Router router;
+
+    public abstract void start();
+
+    protected Router router() {
+        return router;
+    }
+
+    Router start(Vertx vertx) {
+        router = Router.router(vertx);
+        start();
+        return router;
+    }
+}

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

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

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

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

+ 6 - 2
src/main/java/cz/senslog/analyzer/server/vertx/VertxHandlersModule.java

@@ -1,6 +1,7 @@
 package cz.senslog.analyzer.server.vertx;
 
 import cz.senslog.analyzer.persistence.RepositoryModule;
+import cz.senslog.analyzer.server.handler.GroupsHandler;
 import cz.senslog.analyzer.server.handler.InfoHandler;
 import cz.senslog.analyzer.server.handler.StatisticsHandler;
 import dagger.Binds;
@@ -12,8 +13,11 @@ import dagger.Module;
 public abstract class VertxHandlersModule {
 
     @Binds
-    public abstract VertxAbstractHandler provideExampleHandler(InfoHandler handler);
+    public abstract AbstractRestHandler provideInfoHandler(InfoHandler handler);
 
     @Binds
-    public abstract VertxAbstractHandler provideStatisticsHandler(StatisticsHandler handler);
+    public abstract AbstractRestHandler provideStatisticsHandler(StatisticsHandler handler);
+
+    @Binds
+    public abstract AbstractRestHandler provideGroupsHandler(GroupsHandler handler);
 }

+ 19 - 20
src/main/java/cz/senslog/analyzer/server/vertx/VertxServer.java

@@ -1,6 +1,7 @@
 package cz.senslog.analyzer.server.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 io.vertx.core.*;
@@ -10,43 +11,41 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import javax.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
 
 
 public class VertxServer extends AbstractVerticle implements Server {
 
     private static final Logger logger = LogManager.getLogger(VertxServer.class);
 
-    private final InfoHandler infoHandler;
-    private final StatisticsHandler statisticsHandler;
-
-    private Vertx vertx;
+    private final Map<String, AbstractRestHandler> restHandlers;
 
     @Inject
     public VertxServer(
             InfoHandler infoHandler,
-            StatisticsHandler statisticsHandler
+            StatisticsHandler statisticsHandler,
+            GroupsHandler groupsHandler
     ) {
-        this.infoHandler = infoHandler;
-        this.statisticsHandler = statisticsHandler;
+        this.restHandlers = new HashMap<>();
+
+        registerRestHandler("info", infoHandler);
+        registerRestHandler("analytics", statisticsHandler);
+        registerRestHandler("groups", groupsHandler);
+    }
+
+    private void registerRestHandler(String id, AbstractRestHandler handler) {
+        id = id.charAt(0) == '/' ? id : "/" + id;
+        restHandlers.put(id, handler);
     }
 
     @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);
+        for (Map.Entry<String, AbstractRestHandler> handlerEntry : restHandlers.entrySet()) {
+            router.mountSubRouter(handlerEntry.getKey(), handlerEntry.getValue().start(vertx));
+        }
 
         vertx.createHttpServer()
                 .requestHandler(router)