Lukas Cerny 4 năm trước cách đây
mục cha
commit
9379aea70c
37 tập tin đã thay đổi với 1232 bổ sung64 xóa
  1. 3 1
      build.gradle
  2. 13 0
      docker/Dockerfile
  3. 7 0
      docker/start.sh
  4. 29 20
      src/main/java/cz/senslog/watchdog/app/Watcher.java
  5. 2 2
      src/main/java/cz/senslog/watchdog/config/Configuration.java
  6. 1 0
      src/main/java/cz/senslog/watchdog/config/DataProviderConfig.java
  7. 12 2
      src/main/java/cz/senslog/watchdog/config/MessageBrokerConfig.java
  8. 15 0
      src/main/java/cz/senslog/watchdog/config/PropertyConfig.java
  9. 35 0
      src/main/java/cz/senslog/watchdog/config/WebServiceConfig.java
  10. 7 3
      src/main/java/cz/senslog/watchdog/messagebroker/ConsoleMessageBroker.java
  11. 62 22
      src/main/java/cz/senslog/watchdog/messagebroker/EmailMessageBroker.java
  12. 1 1
      src/main/java/cz/senslog/watchdog/messagebroker/MessageBroker.java
  13. 9 8
      src/main/java/cz/senslog/watchdog/messagebroker/MessageStatus.java
  14. 27 0
      src/main/java/cz/senslog/watchdog/messagebroker/Report.java
  15. 23 0
      src/main/java/cz/senslog/watchdog/messagebroker/SimpleReport.java
  16. 5 0
      src/main/java/cz/senslog/watchdog/messagebroker/StatusReport.java
  17. 2 1
      src/main/java/cz/senslog/watchdog/provider/DataProvider.java
  18. 11 2
      src/main/java/cz/senslog/watchdog/provider/ObservationInfo.java
  19. 4 0
      src/main/java/cz/senslog/watchdog/provider/Record.java
  20. 1 0
      src/main/java/cz/senslog/watchdog/provider/database/DatabaseDataProvider.java
  21. 1 0
      src/main/java/cz/senslog/watchdog/provider/database/SensLogRepository.java
  22. 56 2
      src/main/java/cz/senslog/watchdog/provider/ws/WebServiceDataProvider.java
  23. 202 0
      src/main/java/cz/senslog/watchdog/util/http/HttpClient.java
  24. 22 0
      src/main/java/cz/senslog/watchdog/util/http/HttpCode.java
  25. 16 0
      src/main/java/cz/senslog/watchdog/util/http/HttpContentType.java
  26. 10 0
      src/main/java/cz/senslog/watchdog/util/http/HttpHeader.java
  27. 5 0
      src/main/java/cz/senslog/watchdog/util/http/HttpMethod.java
  28. 94 0
      src/main/java/cz/senslog/watchdog/util/http/HttpRequest.java
  29. 69 0
      src/main/java/cz/senslog/watchdog/util/http/HttpRequestBuilder.java
  30. 70 0
      src/main/java/cz/senslog/watchdog/util/http/HttpResponse.java
  31. 42 0
      src/main/java/cz/senslog/watchdog/util/http/HttpResponseBuilder.java
  32. 113 0
      src/main/java/cz/senslog/watchdog/util/http/URLBuilder.java
  33. 219 0
      src/main/java/cz/senslog/watchdog/util/json/BasicJson.java
  34. 22 0
      src/main/java/cz/senslog/watchdog/util/json/BasicJsonDeserializer.java
  35. 6 0
      src/main/java/cz/senslog/watchdog/util/json/FormatFunction.java
  36. 8 0
      src/main/java/cz/senslog/watchdog/util/json/ParseException.java
  37. 8 0
      src/main/java/cz/senslog/watchdog/util/json/SyntaxException.java

+ 3 - 1
build.gradle

@@ -36,6 +36,8 @@ dependencies {
     compile group: 'org.yaml', name: 'snakeyaml', version: '1.24'
     compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.12.0'
     compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.12.0'
+    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.9'
+    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
 
     compile group: 'org.jdbi', name: 'jdbi3-postgres', version: '3.12.2'
     compile group: 'org.jdbi', name: 'jdbi3-jodatime2', version: '3.12.2'
@@ -45,5 +47,5 @@ dependencies {
     compile group: 'javax.mail', name: 'javax.mail-api', version: '1.6.2'
     compile group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2'
 
-    testCompile group: 'junit', name: 'junit', version: '4.12'
+    testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.4.0'
 }

+ 13 - 0
docker/Dockerfile

@@ -0,0 +1,13 @@
+FROM java:8-alpine
+
+ARG config_file
+
+ENV APP_PARAMS "-cf config/$config_file"
+
+COPY docker/start.sh /app/
+COPY build/libs/ /app/bin
+COPY config/$config_file /app/config/$config_file
+
+WORKDIR /app
+
+ENTRYPOINT ["/bin/sh", "-C", "start.sh"]

+ 7 - 0
docker/start.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+BUILD_FOLDER="bin"
+MAIN_CLASS="cz.senslog.watchdog.app.Main"
+LOG_PATH="/var/log/watchdog-app"
+
+java -cp "$BUILD_FOLDER/*" -DlogPath=$LOG_PATH $MAIN_CLASS $APP_PARAMS

+ 29 - 20
src/main/java/cz/senslog/watchdog/app/Watcher.java

@@ -2,16 +2,20 @@ package cz.senslog.watchdog.app;
 
 import cz.senslog.watchdog.config.MonitoredObjectsConfig;
 import cz.senslog.watchdog.messagebroker.MessageBroker;
+import cz.senslog.watchdog.messagebroker.Report;
+import cz.senslog.watchdog.messagebroker.SimpleReport;
 import cz.senslog.watchdog.provider.DataProvider;
 import cz.senslog.watchdog.provider.Record;
+import cz.senslog.watchdog.util.Tuple;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.time.Instant;
 import java.util.List;
-import java.util.Optional;
+import java.util.stream.Collectors;
 
-import static java.lang.String.format;
+import static cz.senslog.watchdog.messagebroker.StatusReport.FAIL;
+import static cz.senslog.watchdog.messagebroker.StatusReport.OK;
 
 public class Watcher {
 
@@ -33,24 +37,29 @@ public class Watcher {
 
     public void check() {
         Instant now = Instant.now();
-        List<Record> records = dataProvider.getLastRecords();
-        for (Record record : records) {
-            Optional<Long> intervalOpt = config.getInterval(record);
-            if (intervalOpt.isPresent() && !record.isValid(intervalOpt.get(), now)) {
-                String message = format("The monitored object '%s' does not contain a new data.", record);
-                messageBroker.send(message, status -> {
-                    String brokerType = messageBroker.getType().name().toLowerCase();
-                    if (status.isSuccess()) {
-                        logger.info("The message '{}' was send via '{}' successfully.",
-                                status.getMessage(), brokerType
-                        );
-                    } else {
-                        logger.error("Can not send a message '{}' via '{}' because of '{}'.",
-                                status.getMessage(), brokerType, status.getError()
-                        );
-                    }
-                });
+
+        List<SimpleReport> reports = dataProvider.getLastRecords().parallelStream()
+                .map(r -> Tuple.of(r, config.getInterval(r)))
+                .filter(t -> t.getItem2().isPresent())
+                .map(t -> new SimpleReport(t.getItem1(), t.getItem1().isValid(t.getItem2().get(), now) ? OK : FAIL))
+                .collect(Collectors.toList());
+
+        List<Record> recordsToReport = reports.stream() // TODO temporary solution
+                .filter(r -> r.getStatus().equals(FAIL))
+                .map(SimpleReport::getRecord)
+                .collect(Collectors.toList());
+
+        messageBroker.send(new Report(now, recordsToReport), status -> {
+            String brokerType = messageBroker.getType().name().toLowerCase();
+            if (status.isSuccess()) {
+                logger.info("The report at '{}' was send via '{}' broker successfully.",
+                        status.getReport().getCreated(), brokerType
+                );
+            } else {
+                logger.error("Can not send a message '{}' via '{}' broker because of '{}'.",
+                        status.getReport().getCreated(), brokerType, status.getError()
+                );
             }
-        }
+        });
     }
 }

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

@@ -93,8 +93,8 @@ public class Configuration {
         PropertyConfig propertyConfig = new PropertyConfig(propertyName);
 
         for (Map.Entry<?, ?> entry : generalConfigMap.entrySet()) {
-            Object keyName = entry.getKey();
-            propertyConfig.setProperty(keyName.toString().toLowerCase(), entry.getValue());
+            String keyName = entry.getKey().toString();
+            propertyConfig.setProperty(keyName, entry.getValue());
         }
         return propertyConfig;
     }

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

@@ -10,6 +10,7 @@ public class DataProviderConfig {
 
         switch (type) {
             case DATABASE: return DatabaseConfig.create(propertyConfig);
+            case WEB_SERVICE: return WebServiceConfig.create(propertyConfig);
         }
 
         return new DataProviderConfig(type);

+ 12 - 2
src/main/java/cz/senslog/watchdog/config/MessageBrokerConfig.java

@@ -4,15 +4,21 @@ public class MessageBrokerConfig {
 
     private final MessageBrokerType type;
 
+    private int vanishPeriodDays;
+
     public static MessageBrokerConfig create(PropertyConfig config) {
         MessageBrokerType type = MessageBrokerType.of(config.getStringProperty("type"));
+        int vanishPeriodDays = config.getIntegerProperty("vanishPeriod", 0);
         PropertyConfig propertyConfig = config.getPropertyConfig("config");
-
+        MessageBrokerConfig messageBrokerConfig;
         switch (type) {
             case EMAIL: return EmailMessageBrokerConfig.create(propertyConfig);
+            default: messageBrokerConfig = new MessageBrokerConfig(type);
         }
 
-        return new MessageBrokerConfig(type);
+        messageBrokerConfig.vanishPeriodDays = vanishPeriodDays;
+
+        return messageBrokerConfig;
     }
 
     protected MessageBrokerConfig(MessageBrokerType type) {
@@ -23,4 +29,8 @@ public class MessageBrokerConfig {
     public MessageBrokerType getType() {
         return type;
     }
+
+    public int getVanishPeriodDays() {
+        return vanishPeriodDays;
+    }
 }

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

@@ -112,6 +112,21 @@ public class PropertyConfig {
     }
 
     /**
+     * Returns property as an Integer. If the property is null, will return default value.
+     * @param name - name of property.
+     * @param defaultValue - default value if the property is null
+     * @return integer value
+     */
+    public Integer getIntegerProperty(String name, int defaultValue) {
+        Object value = getProperty(name);
+        if (value instanceof Integer) {
+            return (Integer)value;
+        }
+
+        return defaultValue;
+    }
+
+    /**
      * Returns property as a LocalDateTime.
      * @param name - name of property.
      * @return localDateTime value.

+ 35 - 0
src/main/java/cz/senslog/watchdog/config/WebServiceConfig.java

@@ -0,0 +1,35 @@
+package cz.senslog.watchdog.config;
+
+public class WebServiceConfig extends DataProviderConfig {
+
+    private final String url;
+    private final String group;
+    private final String user;
+
+    public static DataProviderConfig create(PropertyConfig config) {
+        return new WebServiceConfig(
+                config.getStringProperty("url"),
+                config.getStringProperty("group"),
+                config.getStringProperty("user")
+        );
+    }
+
+    public WebServiceConfig(String url, String group, String user) {
+        super(DataProviderType.WEB_SERVICE);
+        this.url = url;
+        this.group = group;
+        this.user = user;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+
+    public String getUser() {
+        return user;
+    }
+}

+ 7 - 3
src/main/java/cz/senslog/watchdog/messagebroker/ConsoleMessageBroker.java

@@ -9,9 +9,13 @@ public class ConsoleMessageBroker implements MessageBroker {
     private static final Logger logger = LogManager.getLogger(ConsoleMessageBroker.class);
 
     @Override
-    public void send(String text, MessageBrokerHandler status) {
-        logger.warn("Sending a message to the console: '{}'.", text);
-        status.handle(new MessageStatus(text));
+    public void send(Report report, MessageBrokerHandler status) {
+        logger.info("Sending a report created at {} to the console.", report.getCreated());
+        logger.info("Record({}): {};{};{}", "?", "unitId", "sensorId", "lastObservation");
+        for (int i = 0; i < report.getRecords().size(); i++) {
+            logger.info("Record({}): {}", i+1, report.getRecords().get(i).getMessage());
+        }
+        status.handle(new MessageStatus(report));
     }
 
     @Override

+ 62 - 22
src/main/java/cz/senslog/watchdog/messagebroker/EmailMessageBroker.java

@@ -2,31 +2,33 @@ package cz.senslog.watchdog.messagebroker;
 
 import cz.senslog.watchdog.config.EmailMessageBrokerConfig;
 import cz.senslog.watchdog.config.MessageBrokerType;
+import cz.senslog.watchdog.provider.Record;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import javax.mail.*;
 import javax.mail.internet.*;
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
+import java.time.Instant;
+import java.util.*;
 
 import static javax.mail.Message.RecipientType.TO;
 
 public class EmailMessageBroker implements MessageBroker {
 
+    private static final String BREAK_LINE = "<br />";
+
     private static final Logger logger = LogManager.getLogger(EmailMessageBroker.class);
 
     private final EmailMessageBrokerConfig config;
     private final Session session;
 
-    private final Map<String, LocalDateTime> sendMessages;
+    private final Set<Record> sendRecords;
+
 
     public EmailMessageBroker(EmailMessageBrokerConfig config) {
         this.config = config;
         this.session = openSession(config);
-        this.sendMessages = new HashMap<>();
+        this.sendRecords = new HashSet<>();
     }
 
     private Session openSession(EmailMessageBrokerConfig config) {
@@ -45,14 +47,27 @@ public class EmailMessageBroker implements MessageBroker {
         return Session.getInstance(props, auth);
     }
 
-    private Message createMessage(String text) throws MessagingException {
+    private Message createMessage(Instant created, List<Record> records) throws MessagingException {
         Message message = new MimeMessage(session);
         message.setFrom(new InternetAddress(config.getSender()));
         message.setRecipients(TO, InternetAddress.parse(config.getRecipient()));
         message.setSubject(config.getSubject());
 
         MimeBodyPart mimeBodyPart = new MimeBodyPart();
-        mimeBodyPart.setContent(text, "text/html");
+        StringBuilder content = new StringBuilder();
+        content.append(String.format("Data reported at the time <b>%s</b>", created))
+                .append(BREAK_LINE).append(BREAK_LINE);
+
+        content.append("<b>") // TODO refactor
+                .append("unitId").append(";")
+                .append("sensorId").append(";")
+                .append("lastObservation")
+                .append("</b>").append(BREAK_LINE);
+
+        for (Record record : records) {
+            content.append(record.getMessage()).append(BREAK_LINE);
+        }
+        mimeBodyPart.setContent(content.toString(), "text/html");
 
         Multipart multipart = new MimeMultipart();
         multipart.addBodyPart(mimeBodyPart);
@@ -61,35 +76,60 @@ public class EmailMessageBroker implements MessageBroker {
         return message;
     }
 
+    private List<Record> prepareRecords(Report report) {
+        List<Record> readyToSend = new ArrayList<>();
+        for (Record record : report.getRecords()) {
+            if (!sendRecords.contains(record)) {
+                readyToSend.add(record);
+            }
+        }
+        return readyToSend;
+    }
+
     @Override
-    public void send(String text, MessageBrokerHandler status) {
-        if (sendMessages.containsKey(text)) {
-            logger.info("The message '{}' was already send at {}.", text, sendMessages.get(text));
-            status.handle(new MessageStatus(text)); return;
+    public void send(Report report, MessageBrokerHandler status) {
+        if (report == null) {
+            logger.info("Nothing to send. The receive report is null.");
+            status.handle(new MessageStatus(null, "No report to send.")); return;
+        }
+
+        List<Record> recordsToSend = prepareRecords(report);
+        if (recordsToSend.isEmpty()) {
+            logger.info("The same report was already send.");
+            status.handle(new MessageStatus(report)); return;
         }
 
         try {
-            Message message = createMessage(text);
+            Message message = createMessage(report.getCreated(), recordsToSend);
             logger.info("Sending a message via email.");
             Transport.send(message);
             logger.info("The message was send successfully.");
-            sendMessages.put(text, LocalDateTime.now());
-            status.handle(new MessageStatus(text));
+            sendRecords.addAll(recordsToSend);
+            status.handle(new MessageStatus(report));
         } catch (MessagingException e) {
             logger.catching(e);
-            status.handle(new MessageStatus(text, e.getMessage()));
+            status.handle(new MessageStatus(report, e.getMessage()));
         }
 
-        LocalDateTime deadline = LocalDateTime.now().minusDays(1);
-        for (Map.Entry<String, LocalDateTime> entry : sendMessages.entrySet()) {
-            if (entry.getValue().isBefore(deadline)) {
-                sendMessages.remove(entry.getKey());
-            }
-        }
+        vanish();
     }
 
     @Override
     public MessageBrokerType getType() {
         return MessageBrokerType.EMAIL;
     }
+
+    public Record[] vanish() {
+        Instant deadline = Instant.now().minusSeconds(60 * 60 * 24 * config.getVanishPeriodDays());
+        List<Record> deletedRecords = new ArrayList<>();
+        Iterator<Record> iterator = sendRecords.iterator();
+        while(iterator.hasNext()) {
+            Record record = iterator.next();
+            if (record.getTimestamp().isBefore(deadline)) {
+                deletedRecords.add(record);
+                iterator.remove();
+            }
+        }
+        return deletedRecords.toArray(new Record[0]);
+    }
 }

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

@@ -16,7 +16,7 @@ public interface MessageBroker {
         }
     }
 
-    void send(String message, MessageBrokerHandler status);
+    void send(Report report, MessageBrokerHandler status);
 
     MessageBrokerType getType();
 }

+ 9 - 8
src/main/java/cz/senslog/watchdog/messagebroker/MessageStatus.java

@@ -2,19 +2,23 @@ package cz.senslog.watchdog.messagebroker;
 
 public class MessageStatus {
 
-    private final String message;
+    private final Report report;
     private final String error;
 
-    public MessageStatus(String message) {
-        this.message = message;
+    public MessageStatus(Report report) {
+        this.report = report;
         this.error = null;
     }
 
-    public MessageStatus(String message, String error) {
-        this.message = message;
+    public MessageStatus(Report report, String error) {
+        this.report = report;
         this.error = error;
     }
 
+    public Report getReport() {
+        return report;
+    }
+
     public boolean isSuccess() {
         return error == null;
     }
@@ -23,9 +27,6 @@ public class MessageStatus {
         return !isSuccess();
     }
 
-    public String getMessage() {
-        return message;
-    }
 
     public String getError() {
         return error;

+ 27 - 0
src/main/java/cz/senslog/watchdog/messagebroker/Report.java

@@ -0,0 +1,27 @@
+package cz.senslog.watchdog.messagebroker;
+
+import cz.senslog.watchdog.provider.Record;
+
+import java.time.Instant;
+import java.util.List;
+
+public class Report {
+
+    private final Instant created;
+
+    private final List<Record> records;
+
+    public Report(Instant created, List<Record> records) {
+        this.created = created;
+        this.records = records;
+    }
+
+    public Instant getCreated() {
+        return created;
+    }
+
+    public List<Record> getRecords() {
+        return records;
+    }
+
+}

+ 23 - 0
src/main/java/cz/senslog/watchdog/messagebroker/SimpleReport.java

@@ -0,0 +1,23 @@
+package cz.senslog.watchdog.messagebroker;
+
+import cz.senslog.watchdog.provider.Record;
+
+public class SimpleReport {
+
+    private final Record record;
+
+    private final StatusReport status;
+
+    public SimpleReport(Record record, StatusReport status) {
+        this.record = record;
+        this.status = status;
+    }
+
+    public Record getRecord() {
+        return record;
+    }
+
+    public StatusReport getStatus() {
+        return status;
+    }
+}

+ 5 - 0
src/main/java/cz/senslog/watchdog/messagebroker/StatusReport.java

@@ -0,0 +1,5 @@
+package cz.senslog.watchdog.messagebroker;
+
+public enum StatusReport {
+    OK, FAIL
+}

+ 2 - 1
src/main/java/cz/senslog/watchdog/provider/DataProvider.java

@@ -2,6 +2,7 @@ package cz.senslog.watchdog.provider;
 
 import cz.senslog.watchdog.config.DataProviderConfig;
 import cz.senslog.watchdog.config.DatabaseConfig;
+import cz.senslog.watchdog.config.WebServiceConfig;
 import cz.senslog.watchdog.provider.database.DatabaseDataProvider;
 import cz.senslog.watchdog.provider.ws.WebServiceDataProvider;
 
@@ -12,7 +13,7 @@ public interface DataProvider {
     static DataProvider create(DataProviderConfig config) {
         switch (config.getType()) {
             case DATABASE: return new DatabaseDataProvider((DatabaseConfig) config);
-            case WEB_SERVICE: return new WebServiceDataProvider();
+            case WEB_SERVICE: return new WebServiceDataProvider((WebServiceConfig) config);
             default: return null;
         }
     }

+ 11 - 2
src/main/java/cz/senslog/watchdog/provider/database/ObservationInfo.java → src/main/java/cz/senslog/watchdog/provider/ObservationInfo.java

@@ -1,6 +1,5 @@
-package cz.senslog.watchdog.provider.database;
+package cz.senslog.watchdog.provider;
 
-import cz.senslog.watchdog.provider.Record;
 import cz.senslog.watchdog.util.Tuple;
 
 import java.time.Instant;
@@ -49,6 +48,16 @@ public class ObservationInfo implements Record {
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp.toInstant();
+    }
+
+    @Override
+    public String getMessage() {
+        return String.format("%s;%s;%s", unitId, sensorId, timestamp);
+    }
+
+    @Override
     public boolean isValid(long interval, Instant now) {
         return timestamp.plusSeconds(interval).toInstant().isAfter(now);
     }

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

@@ -9,5 +9,9 @@ public interface Record {
 
     Tuple<String, String> getSource();
 
+    Instant getTimestamp();
+
+    String getMessage();
+
     boolean isValid(long interval, Instant now);
 }

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

@@ -2,6 +2,7 @@ package cz.senslog.watchdog.provider.database;
 
 import cz.senslog.watchdog.config.DatabaseConfig;
 import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.provider.ObservationInfo;
 import cz.senslog.watchdog.provider.Record;
 import org.jdbi.v3.core.Jdbi;
 

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

@@ -1,5 +1,6 @@
 package cz.senslog.watchdog.provider.database;
 
+import cz.senslog.watchdog.provider.ObservationInfo;
 import org.jdbi.v3.core.Jdbi;
 
 import java.time.OffsetDateTime;

+ 56 - 2
src/main/java/cz/senslog/watchdog/provider/ws/WebServiceDataProvider.java

@@ -1,15 +1,69 @@
 package cz.senslog.watchdog.provider.ws;
 
+import com.google.gson.reflect.TypeToken;
+import cz.senslog.watchdog.config.WebServiceConfig;
 import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.provider.ObservationInfo;
 import cz.senslog.watchdog.provider.Record;
+import cz.senslog.watchdog.util.http.HttpClient;
+import cz.senslog.watchdog.util.http.HttpRequest;
+import cz.senslog.watchdog.util.http.HttpResponse;
+import cz.senslog.watchdog.util.http.URLBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
-import java.util.Collections;
-import java.util.List;
+import java.lang.reflect.Type;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static cz.senslog.watchdog.util.json.BasicJson.jsonToObject;
+import static java.time.format.DateTimeFormatter.ofPattern;
 
 public class WebServiceDataProvider implements DataProvider {
 
+    private static final Logger logger = LogManager.getLogger(WebServiceDataProvider.class);
+
+    private static final DateTimeFormatter PATTERN = ofPattern("yyyy-MM-dd HH:mm:ssX");
+
+    private final HttpClient httpClient = HttpClient.newHttpClient();
+    private final WebServiceConfig config;
+
+    public WebServiceDataProvider(WebServiceConfig config) {
+        this.config = config;
+    }
+
     @Override
     public List<Record> getLastRecords() {
+
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(config.getUrl(), "/SensorService")
+                        .addParam("Operation", "GetLastObservations")
+                        .addParam("group", config.getGroup())
+                        .addParam("user", config.getUser())
+                        .build()
+                ).build();
+
+        HttpResponse response = httpClient.send(request);
+
+        if (response.isOk()) {
+            String jsonBody = response.getBody();
+            Type lastObsType = new TypeToken<Collection<Map<String, Object>>>() {}.getType();
+            List<Map<String, Object>> lastObservations = jsonToObject(jsonBody, lastObsType);
+            List<Record> observations = new ArrayList<>(lastObservations.size());
+            for (Map<String, Object> observationMap : lastObservations) {
+                long unitId = ((Double)observationMap.get("unitId")).longValue();
+                long sensorId = ((Double)observationMap.get("sensorId")).longValue();
+                String timestampStr = observationMap.get("timeStamp").toString();
+                OffsetDateTime timestamp = OffsetDateTime.parse(timestampStr, PATTERN);
+                observations.add(new ObservationInfo(unitId, sensorId, timestamp));
+            }
+            return observations;
+        } else {
+            logger.error("code: {}, message: {}", response.getStatus(), response.getBody());
+        }
+
         return Collections.emptyList();
     }
+
 }

+ 202 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpClient.java

@@ -0,0 +1,202 @@
+package cz.senslog.watchdog.util.http;
+
+import cz.senslog.watchdog.util.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpMessage;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.util.EntityUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.http.HttpHeaders.*;
+
+/**
+ * The class {@code HttpClient} represents a wrapper for {@link org.apache.http.client.HttpClient}.
+ * Provides functionality of sending GET and POST request. Otherwise is returned response with {@see #BAD_REQUEST}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpClient {
+
+    /** Instance of http client. */
+    private final org.apache.http.client.HttpClient client;
+
+    /**
+     * Factory method to create a new instance of client.
+     * @return new instance of {@code HttpClient}.
+     */
+    public static HttpClient newHttpClient() {
+        return new HttpClient(HttpClientBuilder.create());
+    }
+
+    public static HttpClient newHttpSSLClient() {
+        try {
+            SSLContextBuilder builder = new SSLContextBuilder();
+            builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
+
+            SSLConnectionSocketFactory sslSF = new SSLConnectionSocketFactory(builder.build(),
+                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+            return new HttpClient(HttpClientBuilder.create().setSSLSocketFactory(sslSF));
+
+        } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Private constructors sets http client.
+     */
+    private HttpClient(HttpClientBuilder httpClientBuilder) {
+        this.client = httpClientBuilder.build();
+    }
+
+    /**
+     * Sends http request.
+     * @param request - virtual request.
+     * @return virtual response.
+     */
+    public HttpResponse send(HttpRequest request) {
+        try {
+            switch (request.getMethod()) {
+                case GET:  return sendGet(request);
+                case POST: return sendPost(request);
+                default: return HttpResponse.newBuilder()
+                            .body("Request does not contain method definition.")
+                            .status(HttpCode.METHOD_NOT_ALLOWED).build();
+            }
+        } catch (URISyntaxException e) {
+            return HttpResponse.newBuilder()
+                    .body(e.getMessage()).status(HttpCode.BAD_REQUEST)
+                    .build();
+        } catch (IOException e) {
+            return  HttpResponse.newBuilder()
+                    .body(e.getMessage()).status(HttpCode.SERVER_ERROR)
+                    .build();
+        }
+    }
+
+    /**
+     * Sends GET request.
+     * @param request - virtual request.
+     * @return virtual response of the request.
+     * @throws URISyntaxException throws if host url is not valid.
+     * @throws IOException throws if anything happen during sending.
+     */
+    private HttpResponse sendGet(HttpRequest request) throws IOException, URISyntaxException {
+
+        URI uri = request.getUrl().toURI();
+        HttpGet requestGet = new HttpGet(uri);
+        setBasicHeaders(request, requestGet);
+
+        org.apache.http.HttpResponse responseGet = client.execute(requestGet);
+
+        HttpResponse response = HttpResponse.newBuilder()
+                .status(responseGet.getStatusLine().getStatusCode())
+                .headers(getHeaders(responseGet))
+                .body(getBody(responseGet.getEntity()))
+                .build();
+
+        EntityUtils.consume(responseGet.getEntity());
+
+        return response;
+    }
+
+    /**
+     * Sends POST request.
+     * @param request - virtual request.
+     * @return virtual response of the request.
+     * @throws URISyntaxException throws if host url is not valid.
+     * @throws IOException throws if anything happen during sending.
+     */
+    private HttpResponse sendPost(HttpRequest request) throws URISyntaxException, IOException {
+
+        URI uri = request.getUrl().toURI();
+        HttpPost requestPost = new HttpPost(uri);
+        setBasicHeaders(request, requestPost);
+
+        if (StringUtils.isNotBlank(request.getContentType())) {
+            requestPost.setHeader(CONTENT_TYPE, request.getContentType());
+        }
+
+        requestPost.setEntity(new StringEntity(request.getBody()));
+
+        org.apache.http.HttpResponse responsePost = client.execute(requestPost);
+
+        HttpResponse response = HttpResponse.newBuilder()
+                .headers(getHeaders(requestPost))
+                .status(responsePost.getStatusLine().getStatusCode())
+                .body(getBody(responsePost.getEntity()))
+                .build();
+
+        EntityUtils.consume(responsePost.getEntity());
+
+        return response;
+    }
+
+    /**
+     * Sets basic headers to each request.
+     * @param userRequest - virtual request.
+     * @param httpRequest - real request prepared to send.
+     */
+    private void setBasicHeaders(HttpRequest userRequest, HttpRequestBase httpRequest) {
+
+        httpRequest.setHeader(USER_AGENT, "SenslogConnector/1.0");
+        httpRequest.setHeader(CACHE_CONTROL, "no-cache");
+
+        for (Map.Entry<String, String> headerEntry : userRequest.getHeaders().entrySet()) {
+            httpRequest.setHeader(headerEntry.getKey(), headerEntry.getValue());
+        }
+    }
+
+    /**
+     * Returns map of headers from the response.
+     * @param response - response message.
+     * @return map of headers.
+     */
+    private Map<String, String> getHeaders(HttpMessage response) {
+        Map<String, String> headers = new HashMap<>();
+        for (Header header : response.getAllHeaders()) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    /**
+     * Returns body from the response.
+     * @param entity - response entity.
+     * @return string body of the response.
+     * @throws IOException can not get body from the response.
+     */
+    private String getBody(HttpEntity entity) throws IOException {
+        if (entity == null) return "";
+        InputStream contentStream = entity.getContent();
+        InputStreamReader bodyStream = new InputStreamReader(contentStream);
+        BufferedReader rd = new BufferedReader(bodyStream);
+        StringBuilder bodyBuffer = new StringBuilder();
+        String line;
+        while ((line = rd.readLine()) != null) {
+            bodyBuffer.append(line);
+        }
+        return bodyBuffer.toString();
+    }
+}

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

@@ -0,0 +1,22 @@
+package cz.senslog.watchdog.util.http;
+
+public class HttpCode {
+
+    public static final int OK = 200;
+
+    public static final int NO_RESULT = 204;
+
+    public static final int NO_CONTENT = 204;
+
+    public static final int BAD_REQUEST = 400;
+
+    public static final int UNAUTHORIZED = 401;
+
+    public static final int FORBIDDEN = 403;
+
+    public static final int NOT_FOUND = 404;
+
+    public static final int METHOD_NOT_ALLOWED = 405;
+
+    public static final int SERVER_ERROR = 500;
+}

+ 16 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpContentType.java

@@ -0,0 +1,16 @@
+package cz.senslog.watchdog.util.http;
+
+public final class HttpContentType {
+    public final static String APPLICATION_ATOM_XML         = "application/atom+xml";
+    public final static String APPLICATION_FORM_URLENCODED  = "application/x-www-form-urlencoded";
+    public final static String APPLICATION_JSON             = "application/json";
+    public final static String APPLICATION_OCTET_STREAM     = "application/octet-stream";
+    public final static String APPLICATION_SVG_XML          = "application/svg+xml";
+    public final static String APPLICATION_XHTML_XML        = "application/xhtml+xml";
+    public final static String APPLICATION_XML              = "application/xml";
+    public final static String MULTIPART_FORM_DATA          = "multipart/form-data";
+    public final static String TEXT_HTML                    = "text/html";
+    public final static String TEXT_PLAIN                   = "text/plain";
+    public final static String TEXT_XML                     = "text/xml";
+    public final static String WILDCARD                     = "*/*";
+}

+ 10 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpHeader.java

@@ -0,0 +1,10 @@
+package cz.senslog.watchdog.util.http;
+
+import org.apache.http.HttpHeaders;
+
+public final class HttpHeader {
+    public static final String AUTHORIZATION = HttpHeaders.AUTHORIZATION;
+    public static final String DATE = HttpHeaders.DATE;
+    public static final String ACCEPT = HttpHeaders.ACCEPT;
+    public static final String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE;
+}

+ 5 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpMethod.java

@@ -0,0 +1,5 @@
+package cz.senslog.watchdog.util.http;
+
+public enum  HttpMethod {
+    GET, POST
+}

+ 94 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpRequest.java

@@ -0,0 +1,94 @@
+package cz.senslog.watchdog.util.http;
+
+import java.net.URL;
+import java.util.Map;
+
+/**
+ * The class {@code HttpRequest} represents a wrapper for a http request.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpRequest {
+
+    public interface Builder {
+        Builder header(String name, String value);
+        Builder url(URL url);
+        Builder POST();
+        Builder GET();
+        Builder contentType(String contentType);
+        Builder body(String body);
+        HttpRequest build();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpRequest}.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder() {
+        return new HttpRequestBuilder();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpRequest}.
+     * @param url - host url.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder(URL url) {
+        HttpRequestBuilder builder = new HttpRequestBuilder();
+        builder.url(url);
+        return builder;
+    }
+
+    /** Request url. */
+    private final URL url;
+
+    /** Request headers. */
+    private final Map<String, String> headers;
+
+    /** Request body. */
+    private final String body;
+
+    /** Request method. */
+    private final HttpMethod method;
+
+    /** Request content type. */
+    private final String contentType;
+
+    /**
+     * Constructors sets all attributes.
+     * @param url - url.
+     * @param headers - headers.
+     * @param body - body.
+     * @param method - method.
+     * @param contentType - content type.
+     */
+    HttpRequest(URL url, Map<String, String> headers, String body, HttpMethod method, String contentType) {
+        this.url = url;
+        this.headers = headers;
+        this.body = body;
+        this.method = method;
+        this.contentType = contentType;
+    }
+
+    public URL getUrl() {
+        return url;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public HttpMethod getMethod() {
+        return method;
+    }
+
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+}

+ 69 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpRequestBuilder.java

@@ -0,0 +1,69 @@
+package cz.senslog.watchdog.util.http;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The class {@code HttpRequestBuilder} represents a builder for the {@link HttpRequest}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+final class HttpRequestBuilder implements HttpRequest.Builder {
+
+    private final Map<String, String> headers;
+    private URL url;
+    private String body;
+    private HttpMethod method;
+    private String contentType;
+
+    HttpRequestBuilder() {
+        this.headers = new HashMap<>();
+        this.method = HttpMethod.GET;
+        this.body = "";
+    }
+
+
+    @Override
+    public HttpRequest.Builder header(String name, String value) {
+        this.headers.put(name, value);
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder url(URL url) {
+        this.url = url;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder POST() {
+        this.method = HttpMethod.POST;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder GET() {
+        this.method = HttpMethod.GET;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder contentType(String contentType) {
+        this.contentType = contentType;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder body(String body) {
+        this.body = body;
+        return this;
+    }
+
+    @Override
+    public HttpRequest build() {
+        return new HttpRequest(url, headers, body, method, contentType);
+    }
+}

+ 70 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpResponse.java

@@ -0,0 +1,70 @@
+package cz.senslog.watchdog.util.http;
+
+import java.util.Map;
+
+/**
+ * The class {@code HttpResponse} represents a wrapper for a http response.
+ * Contains basic information like status, headers and body.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpResponse {
+
+    public interface Builder {
+        Builder body(String body);
+        Builder headers(Map<String, String> headers);
+        Builder status(int status);
+        HttpResponse build();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpResponse}.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder() {
+        return new HttpResponseBuilder();
+    }
+
+    /** Response body. */
+    private final String body;
+
+    /** Response headers. */
+    private final Map<String, String> headers;
+
+    /** Response status. */
+    private final int status;
+
+    /**
+     * Constructors sets all attributes.
+     * @param body - body.
+     * @param headers - headers.
+     * @param status - status.
+     */
+    HttpResponse(String body, Map<String, String> headers, int status) {
+        this.body = body;
+        this.headers = headers;
+        this.status = status;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public String getHeader(String value) {
+        return headers.get(value);
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public boolean isOk() {
+        return status == HttpCode.OK;
+    }
+
+    public boolean isError() {
+        return !isOk();
+    }
+}

+ 42 - 0
src/main/java/cz/senslog/watchdog/util/http/HttpResponseBuilder.java

@@ -0,0 +1,42 @@
+package cz.senslog.watchdog.util.http;
+
+import java.util.Map;
+
+/**
+ * The class {@code HttpResponseBuilder} represents a builder for the {@link HttpResponse}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class HttpResponseBuilder implements HttpResponse.Builder {
+
+    private String body;
+    private Map<String, String> headers;
+    private int status;
+
+    HttpResponseBuilder(){}
+
+    @Override
+    public HttpResponse.Builder body(String body) {
+        this.body = body;
+        return this;
+    }
+
+    @Override
+    public HttpResponse.Builder headers(Map<String, String> headers) {
+        this.headers = headers;
+        return this;
+    }
+
+    @Override
+    public HttpResponse.Builder status(int status) {
+        this.status = status;
+        return this;
+    }
+
+    @Override
+    public HttpResponse build() {
+        return new HttpResponse(body, headers, status);
+    }
+}

+ 113 - 0
src/main/java/cz/senslog/watchdog/util/http/URLBuilder.java

@@ -0,0 +1,113 @@
+package cz.senslog.watchdog.util.http;
+
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import static java.net.URLEncoder.encode;
+
+/**
+ * The class {@code URLBuilder} represents a builder to create a new instance of {@link URL}.
+ * Provides a creating a url from domain and path and adding a parameter.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class URLBuilder {
+
+    /**
+     * Factory method to create a new instance of {@code URLBuilder} from base url.
+     * @param baseURL - host url.
+     * @return new instance of {@code URLBuilder}.
+     */
+    public static URLBuilder newBuilder(String baseURL) {
+        return new URLBuilder(baseURL);
+    }
+
+    /**
+     * Factory method to create a new instance of {@code URLBuilder} from domain and path.
+     * Normalizes domain and path to the form:
+     *
+     * domain: http://domain.com/
+     * path: /host
+     * -> url: http://domain.com/host
+     *
+     * domain: http://domain.com
+     * path: host
+     * -> url: http://domain.com/host
+     *
+     * @param domain - domain of host.
+     * @param path - path of host.
+     * @return new instance of {@code URLBuilder}.
+     */
+    public static URLBuilder newBuilder(String domain, String path) {
+        boolean domainSlash = domain.endsWith("/");
+        boolean pathSlash = path.startsWith("/");
+
+        if ((domainSlash && !pathSlash) || (!domainSlash && pathSlash)) {
+            return new URLBuilder(domain + path);
+        } else if (domainSlash) {
+            return new URLBuilder(domain + path.substring(1));
+        } else {
+            return new URLBuilder(domain + "/" + path);
+        }
+    }
+
+    /** String builder for url. */
+    private final StringBuilder urlBuilder;
+
+    /** String builder for parameters. */
+    private final StringBuilder paramsBuilder;
+
+    /**
+     * Private constructor initializes builders and normalizes url.
+     * If the url ends with slash '/', it is removed.
+     * @param baseURL - host url.
+     */
+    private URLBuilder(String baseURL) {
+        String url = baseURL.endsWith("/") ? baseURL.substring(0, baseURL.length() - 1) : baseURL;
+        this.urlBuilder = new StringBuilder(url);
+        this.paramsBuilder = new StringBuilder();
+    }
+
+    /**
+     * Adds a new parameter to the url.
+     * @param name - name of parameter.
+     * @param value - value of parameter.
+     * @return instance of {@code URLBuilder}.
+     */
+    public URLBuilder addParam(String name, String value) {
+        try {
+            paramsBuilder.append("&").append(name).append("=").append(encode(value, "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e.getMessage());
+        }
+        return this;
+    }
+
+    /**
+     * Adds a new parameter to the url.
+     * @param name - name of parameter.
+     * @param value - value of parameter.
+     * @return instance of {@code URLBuilder}.
+     */
+    public URLBuilder addParam(String name, Object value) {
+        if (value == null) return this;
+        return addParam(name, value.toString());
+    }
+
+    /**
+     * Creates a new instance of {@link URL}.
+     * @return new instance of {@link URL}.
+     */
+    public URL build() {
+        try {
+            String params = paramsBuilder.replace(0, 1, "").toString();
+            return new URL(urlBuilder.append(params.isEmpty() ? "" : ("?" + params)).toString());
+        } catch (MalformedURLException e) {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
+}
+

+ 219 - 0
src/main/java/cz/senslog/watchdog/util/json/BasicJson.java

@@ -0,0 +1,219 @@
+package cz.senslog.watchdog.util.json;
+
+import com.google.gson.*;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import cz.senslog.watchdog.util.Tuple;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+
+import static com.google.gson.stream.JsonToken.END_DOCUMENT;
+import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+/**
+ * The class {@code BasicJson} represents a basic wrapper for {@link Gson} library.
+ * Provides basic converter from object to string and string to object.
+ *
+ * Configuration contains basic formatters for {@see LocalDateTime}, {@see ZonedDateTime} and {@see Class}.
+ *
+ *
+ * Both time classes are formatter to ISO format e.q. '2011-12-03T10:15:30',
+ * '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30+01:00[Europe/Paris]'.
+ *
+ * Class is formatted as the full name of the class.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class BasicJson {
+
+    /** Instance of json converter. */
+    private static final Gson gson = new GsonBuilder()
+            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+            .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter())
+            .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter())
+            .registerTypeAdapter(Class.class, new ClassAdapter())
+            .create();
+
+    /** Formatter for {@see LocalDateTime}. */
+    private static class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
+
+        @Override
+        public JsonElement serialize(LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(localDateTime.format(ISO_DATE_TIME));
+        }
+
+        @Override
+        public LocalDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return LocalDateTime.parse(jsonElement.getAsString(), ISO_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see ZonedDateTime}. */
+    private static class ZonedDateTimeAdapter implements JsonSerializer<ZonedDateTime>, JsonDeserializer<ZonedDateTime> {
+
+        @Override
+        public JsonElement serialize(ZonedDateTime zonedDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(zonedDateTime.format(ISO_DATE_TIME));
+        }
+
+        @Override
+        public ZonedDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return ZonedDateTime.parse(jsonElement.getAsString(), ISO_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see OffsetDateTime}. */
+    private static class OffsetDateTimeAdapter implements JsonSerializer<OffsetDateTime>, JsonDeserializer<OffsetDateTime> {
+
+        @Override
+        public JsonElement serialize(OffsetDateTime offsetDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(offsetDateTime.format(ISO_OFFSET_DATE_TIME));
+        }
+
+        @Override
+        public OffsetDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return OffsetDateTime.parse(jsonElement.getAsString(), ISO_OFFSET_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see Class}. */
+    private static class ClassAdapter implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {
+
+        @Override
+        public JsonElement serialize(Class<?> aClass, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(aClass.getName());
+        }
+
+        @Override
+        public Class<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            try {
+                return Class.forName(jsonElement.getAsString());
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Deserialize json to a typed object according to class.
+     * @param jsonString - json string.
+     * @param aClass - class of the object.
+     * @param <T> - generic type object.
+     * @return new instance of the input class.
+     */
+    public static <T> T jsonToObject(String jsonString, Class<T> aClass) {
+        try {
+            return gson.fromJson(jsonString, aClass);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Deserialize json to a typed object according to type.
+     * @param jsonString - json string.
+     * @param type - type of the object.
+     * @param <T> - generic type object.
+     * @return new instance of the input type.
+     */
+    public static <T> T jsonToObject(String jsonString, Type type) {
+        try {
+            return gson.fromJson(jsonString, type);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Serialize object to string json.
+     * @param object - input object.
+     * @param <T> - generic type of object.
+     * @return string json.
+     */
+    public static <T> String objectToJson(T object) {
+        try {
+            return gson.toJson(object);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    @SafeVarargs
+    public static <R, E> R jsonToObject(String json, Type type, Tuple<Class<E>, FormatFunction<E>>... formatters) {
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        for (Tuple<Class<E>, FormatFunction<E>> formatter : formatters) {
+            gsonBuilder.registerTypeAdapter(formatter.getItem1(), new BasicJsonDeserializer<>(formatter.getItem2()));
+        }
+        try {
+            Gson gson = gsonBuilder.create();
+            return gson.fromJson(json, type);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        } catch (RuntimeException e) {
+            throw new ParseException(e.getMessage());
+        }
+    }
+
+
+    /**
+     * Checks if input string is in json format.
+     * @param json - input json.
+     * @return true - valid, false - invalid.
+     */
+    public static boolean isValid(String json) {
+        return isValid(new JsonReader(new StringReader(json)));
+    }
+
+    /**
+     * Validates input json reader.
+     * @param jsonReader - input json reader.
+     * @return true - valid, false - invalid.
+     */
+    private static boolean isValid(JsonReader jsonReader) {
+        try {
+            JsonToken token;
+            loop:
+            while ( (token = jsonReader.peek()) != END_DOCUMENT && token != null ) {
+                switch ( token ) {
+                    case BEGIN_ARRAY:
+                        jsonReader.beginArray();
+                        break;
+                    case END_ARRAY:
+                        jsonReader.endArray();
+                        break;
+                    case BEGIN_OBJECT:
+                        jsonReader.beginObject();
+                        break;
+                    case END_OBJECT:
+                        jsonReader.endObject();
+                        break;
+                    case NAME:
+                        jsonReader.nextName();
+                        break;
+                    case STRING:
+                    case NUMBER:
+                    case BOOLEAN:
+                    case NULL:
+                        jsonReader.skipValue();
+                        break;
+                    case END_DOCUMENT:
+                        break loop;
+                    default:
+                        throw new AssertionError(token);
+                }
+            }
+            return true;
+        } catch (IOException ignored ) {
+            return false;
+        }
+    }
+
+}

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

@@ -0,0 +1,22 @@
+package cz.senslog.watchdog.util.json;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+import java.lang.reflect.Type;
+
+public class BasicJsonDeserializer<T> implements JsonDeserializer<T> {
+
+    private final FormatFunction<T> formatter;
+
+    public BasicJsonDeserializer(FormatFunction<T> formatter) {
+        this.formatter = formatter;
+    }
+
+    @Override
+    public T deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+        return formatter.apply(jsonElement.getAsString());
+    }
+}

+ 6 - 0
src/main/java/cz/senslog/watchdog/util/json/FormatFunction.java

@@ -0,0 +1,6 @@
+package cz.senslog.watchdog.util.json;
+
+@FunctionalInterface
+public interface FormatFunction<T> {
+    T apply(String element);
+}

+ 8 - 0
src/main/java/cz/senslog/watchdog/util/json/ParseException.java

@@ -0,0 +1,8 @@
+package cz.senslog.watchdog.util.json;
+
+public class ParseException extends RuntimeException {
+
+    public ParseException(String message) {
+        super(message);
+    }
+}

+ 8 - 0
src/main/java/cz/senslog/watchdog/util/json/SyntaxException.java

@@ -0,0 +1,8 @@
+package cz.senslog.watchdog.util.json;
+
+public class SyntaxException extends RuntimeException {
+
+    public SyntaxException(String message) {
+        super(message);
+    }
+}