فهرست منبع

Prepared architecture, init of OpenAPI, implemented FeederServlet and SensorService

Lukas Cerny 4 سال پیش
والد
کامیت
8d8b65cbd4
31فایلهای تغییر یافته به همراه1288 افزوده شده و 227 حذف شده
  1. 231 0
      doc/openApiDescription.yaml
  2. 8 43
      pom.xml
  3. 0 50
      src/main/java/cz/hsrs/main/StartJetty.java
  4. 2 2
      src/main/java/cz/hsrs/rest/SCListener.java
  5. 1 1
      src/main/java/io/senslog/app/JettyServer.java
  6. 18 0
      src/main/java/io/senslog/app/Main.java
  7. 3 3
      src/main/java/io/senslog/core/AppInfo.java
  8. 2 1
      src/main/java/io/senslog/database/DBRepositoryPool.java
  9. 26 0
      src/main/java/io/senslog/database/model/DBObject.java
  10. 42 0
      src/main/java/io/senslog/database/model/DBObservation.java
  11. 75 0
      src/main/java/io/senslog/database/model/DBPosition.java
  12. 142 8
      src/main/java/io/senslog/database/repository/ObservationRepository.java
  13. 38 0
      src/main/java/io/senslog/database/repository/PositionRepository.java
  14. 37 0
      src/main/java/io/senslog/domain/Latitude.java
  15. 37 0
      src/main/java/io/senslog/domain/Longitude.java
  16. 0 49
      src/main/java/io/senslog/manager/ObservationManager.java
  17. 117 0
      src/main/java/io/senslog/service/DataService.java
  18. 28 0
      src/main/java/io/senslog/util/NumberUtils.java
  19. 18 0
      src/main/java/io/senslog/util/TimestampUtils.java
  20. 40 0
      src/main/java/io/senslog/ws/WSException.java
  21. 21 0
      src/main/java/io/senslog/ws/handler/ExceptionHandler.java
  22. 0 44
      src/main/java/io/senslog/ws/handler/FeederHandler.java
  23. 7 10
      src/main/java/io/senslog/ws/handler/InfoHandler.java
  24. 90 0
      src/main/java/io/senslog/ws/handler/v1/DataPublishingHandler.java
  25. 83 0
      src/main/java/io/senslog/ws/handler/v1/DataRetrievingHandler.java
  26. 6 0
      src/main/java/io/senslog/ws/model/WSObject.java
  27. 68 0
      src/main/java/io/senslog/ws/model/WSObservation.java
  28. 113 0
      src/main/java/io/senslog/ws/model/WSPosition.java
  29. 4 6
      src/main/webapp/WEB-INF/database.properties
  30. 29 8
      src/main/webapp/WEB-INF/web.xml
  31. 2 2
      src/main/webapp/signin.jsp

+ 231 - 0
doc/openApiDescription.yaml

@@ -0,0 +1,231 @@
+openapi: 3.0.3
+info:
+  title: SensLog 2.0
+  version: 1.0.0
+externalDocs:
+  description: Find out more about SensLog
+  url: http://senslog.org
+servers:
+  - url: http://localhost:8080/DBService/api
+tags:
+  - name: "Version 1"
+    description: "Origin version of public API"
+  - name: "Version 2"
+    description: "New version of public API"
+paths:
+  /v1/FeederServlet:
+    post:
+      tags:
+        - Version 1
+      operationId: insertionToSenslog
+      parameters:
+        - name: Operation
+          in: query
+          required: true
+          schema:
+            type: string
+            enum:
+              - InsertObservation
+              - InsertPosition
+              - InsertAlertEvent
+              - SolvingAlertEvent
+        - name: unit_id
+          in: query
+          description: "Identifier of unit. Avaliable only for operation types: [InsertObservation, InsertPosition]"
+          schema:
+            type: integer
+            format: int64
+        - name: sensor_id
+          in: query
+          description: "Identifier of sensor. Avaliable only for operation types: [InsertObservation]"
+          schema:
+            type: integer
+            format: int64
+        - name: date
+          in: query
+          description: "Timestamp of measured value (e.g. 2015-07-15 12:00:00+0200). Avaliable only for operation types: [InsertObservation, InsertPosition]"
+          schema:
+            type: string
+        - name: value
+          in: query
+          description: "Measured value. Avaliable only for operation types: [InsertObservation]"
+          schema:
+            type: number
+            format: double
+        - name: lat
+          in: query
+          description: "Latitude of position. Avaliable only for operation types: [InsertPosition]"
+          schema:
+            type: number
+            format: double
+        - name: lon
+          in: query
+          description: "Longitude of position. Avaliable only for operation types: [InsertPosition]"
+          schema:
+            type: number
+            format: double
+        - name: alt
+          in: query
+          description: "Altitude of position in meters. Avaliable only for operation types: [InsertPosition]"
+          schema:
+            type: number
+            format: double
+        - name: speed
+          in: query
+          description: "Speed of the unit. Avaliable only for operation types: [InsertPosition]"
+          schema:
+            type: number
+            format: double
+        - name: dop
+          in: query
+          description: "Dilution of precision. Avaliable only for operation types: [InsertPosition]"
+          schema:
+            type: integer
+            format: int32
+      responses:
+        200:
+          description: "Successfull operation."
+          content:
+            text/plain:
+              schema:
+                type: boolean
+        400:
+          description: "Invalid parameter value."
+          content:
+            text/plain:
+              schema:
+                type: object
+                properties:
+                  timestamp:
+                    type: integer
+                    description: "Epoch time"
+                    example: 1609459200
+                  message:
+                    type: string
+                    description: "Description of failure."
+                    example: "Parameter 'unit_id' is not a number."
+        406:
+          description: Invalid operation type
+          content:
+            text/plain:
+              schema:
+                type: object
+                properties:
+                  timestamp:
+                    type: integer
+                    description: "Epoch time"
+                    example: 1609459200
+                  message:
+                    type: string
+                    description: "Description of failture."
+                    example: "Unsupported operation 'WrongOperation'."
+  /v1/SensorService:
+    get:
+      tags:
+        - Version 1
+      operationId: sensorService
+      parameters:
+        - name: Operation
+          in: query
+          required: true
+          schema:
+            type: string
+            enum:
+              - GetLastObservations
+              - GetSensors
+              - GetObservations
+        - name: group
+          in: query
+          description: "Name of group of units. Can be used with 'sensor_id' parameter. Avaliable only for operation types: [GetLastObservations]"
+          schema:
+            type: string
+        - name: unit_id
+          in: query
+          description: "Identifier of unit. Can be used with 'sensor_id' parameter. Avaliable only for operation types: [GetLastObservations]"
+          schema:
+            type: integer
+            format: int64
+        - name: sensor_id
+          in: query
+          description: "Identifier of sensor.\n Avaliable only for operation types: [GetLastObservations]"
+          schema:
+            type: integer
+            format: int64
+      responses:
+        200:
+          description: "List of observations."
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    unit_id:
+                      type: integer
+                      format: int64
+                      example: 1230000
+                    sensor_id:
+                      type: integer
+                      format: int64
+                      example: 2830001
+                    value:
+                      type: number
+                      format: double
+                      example: 0.3453
+                    time_stamp:
+                      type: string
+                      example: "2021-03-15 12:00:00+02"
+        400:
+          description: "Invalid parameter value."
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  timestamp:
+                    type: integer
+                    description: "Epoch time"
+                    example: 1609459200
+                  message:
+                    type: string
+                    description: "Description of failture."
+                    example: "Parameter 'unit_id' is not a number."
+        406:
+          description: "Invalid operation type"
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  timestamp:
+                    type: integer
+                    description: "Epoch time"
+                    example: 1609459200
+                  message:
+                    type: string
+                    description: "Description of failture."
+                    example: "Unsupported operation 'WrongOperation'."
+  /info:
+    get:
+      tags:
+        - "Version 2"
+      operationId: info
+      responses:
+        200:
+          description: "General information about running server."
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  uptime:
+                    type: string
+                    example: "10 min 05 sec"
+                  appVersion:
+                    type: string
+                    example: "1.3.5"
+                  buildVersion:
+                    type: string
+                    example: "1609459200"
+components: {}

+ 8 - 43
pom.xml

@@ -49,7 +49,7 @@
                     <archive>
                         <manifest>
                             <addClasspath>true</addClasspath>
-                            <mainClass>cz.hsrs.main.StartJetty</mainClass>
+                            <mainClass>io.senslog.app.Main</mainClass>
                         </manifest>
                     </archive>
                 </configuration>
@@ -100,9 +100,6 @@
                 <artifactId>maven-war-plugin</artifactId>
                 <version>2.0.2</version>
                 <configuration>
-<!-- HACK for testing -->
-        <skipTests>true</skipTests>
-<!-- HACK for testing -->
                     <archive>
                         <manifest>
                             <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
@@ -160,7 +157,7 @@
     </build>
 
     <dependencies>
-      <!-- NEW DATABASE DEPENDENCIES -->
+      <!-- NEW DEPENDENCIES -->
         <dependency>
             <groupId>com.zaxxer</groupId>
             <artifactId>HikariCP</artifactId>
@@ -176,6 +173,12 @@
             <artifactId>jdbi3-jodatime2</artifactId>
             <version>3.20.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.12.0</version>
+        </dependency>
+
 
 
 
@@ -238,43 +241,6 @@
 
 
 
-
-
-
-        <!--        <dependency>-->
-<!--            <groupId>org.mortbay.jetty</groupId>-->
-<!--            <artifactId>jetty</artifactId>-->
-<!--            <version>${jetty.version}</version>-->
-<!--            <scope>provided</scope>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>org.mortbay.jetty</groupId>-->
-<!--            <artifactId>jetty-servlet-tester</artifactId>-->
-<!--            <version>${jetty.version}</version>-->
-<!--            <scope>test</scope>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>org.mortbay.jetty</groupId>-->
-<!--            <artifactId>jetty-util</artifactId>-->
-<!--            <version>${jetty.version}</version>-->
-<!--            <scope>provided</scope>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>org.mortbay.jetty</groupId>-->
-<!--            <artifactId>jetty-management</artifactId>-->
-<!--            <version>${jetty.version}</version>-->
-<!--            <scope>provided</scope>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>org.mortbay.jetty</groupId>-->
-<!--            <artifactId>jsp-2.1-jetty</artifactId>-->
-<!--            <version>${jetty.version}</version>-->
-<!--            <scope>provided</scope>-->
-<!--        </dependency>-->
-<!-- JETTY -->
-
-
-
         <!-- Provided dependencies -->
         <dependency>
             <groupId>net.sf.jasperreports</groupId>
@@ -326,7 +292,6 @@
     </dependency>
 <!-- JERSEY -->
 
-    <!-- https://mvnrepository.com/artifact/org.jvnet.mimepull/mimepull -->
     <dependency>
         <groupId>org.jvnet.mimepull</groupId>
         <artifactId>mimepull</artifactId>

+ 0 - 50
src/main/java/cz/hsrs/main/StartJetty.java

@@ -1,50 +0,0 @@
-package cz.hsrs.main;
-
-//import org.mortbay.jetty.Connector;
-//import org.mortbay.jetty.Server;
-//import org.mortbay.jetty.bio.SocketConnector;
-//import org.mortbay.jetty.webapp.WebAppContext;
-
-import cz.hsrs.db.pool.SQLExecutor;
-
-public class StartJetty {
-/*
-    public static Server server = new Server();
-
-    public static void start() throws Exception {
-        try {
-            SocketConnector connector = new SocketConnector();
-            connector.setPort(8080);
-
-            server.setConnectors(new Connector[] { connector });
-            WebAppContext context = new WebAppContext();
-            context.setServer(server);
-            context.setContextPath("/senslog15");
-            //context.setContextPath("/");
-            context.setWar("src/main/webapp");
-            server.addHandler(context);
-            SQLExecutor.setConfigFile("WEB-INF/local_logging.properties");
-            
-            server.start();
-            
-        } catch (Exception e) {
-            if (server != null) {
-                try {
-                    server.stop();
-                } catch (Exception e1) {
-                    throw new RuntimeException(e1);
-                }
-            }
-        }
-    }
-    public static void stop() throws Exception {
-        server.stop();
-    }
-
- */
-    public static void main(String[] args) throws Exception {
-    	//PropertyConfigurator.configure("/log4j.properties");
-      //  start();
-        new JettyServer().start();
-    }
-}

+ 2 - 2
src/main/java/cz/hsrs/rest/SCListener.java

@@ -8,8 +8,8 @@ import java.io.FileInputStream;
 import java.util.Properties;
 import java.util.logging.Logger;
 
-import static io.senslog.core.ApplicationInfo.appVersion;
-import static io.senslog.core.ApplicationInfo.buildVersion;
+import static io.senslog.core.AppInfo.appVersion;
+import static io.senslog.core.AppInfo.buildVersion;
 
 /**
  * 

+ 1 - 1
src/main/java/cz/hsrs/main/JettyServer.java → src/main/java/io/senslog/app/JettyServer.java

@@ -1,4 +1,4 @@
-package cz.hsrs.main;
+package io.senslog.app;
 
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.webapp.WebAppContext;

+ 18 - 0
src/main/java/io/senslog/app/Main.java

@@ -0,0 +1,18 @@
+package io.senslog.app;
+
+import io.senslog.database.DBConfig;
+import io.senslog.database.DBRepositoryPool;
+
+public class Main {
+
+    public static void main(String[] args) throws Exception {
+
+        DBConfig dbConfig = new DBConfig(
+                "jdbc:postgresql://localhost:5432/senslog1",
+                "postgres", "root", 6
+        );
+        DBRepositoryPool.create(dbConfig);
+
+        new JettyServer().start();
+    }
+}

+ 3 - 3
src/main/java/io/senslog/core/ApplicationInfo.java → src/main/java/io/senslog/core/AppInfo.java

@@ -6,9 +6,9 @@ import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-public final class ApplicationInfo {
+public final class AppInfo {
 
-    private static final Logger logger = Logger.getLogger(ApplicationInfo.class.getSimpleName());
+    private static final Logger logger = Logger.getLogger(AppInfo.class.getSimpleName());
 
     private static final long startAppEpoch;
     private static final Properties properties;
@@ -19,7 +19,7 @@ public final class ApplicationInfo {
     }
 
     private static Properties loadVersionProperties(String fileName) {
-        ClassLoader classLoader = ApplicationInfo.class.getClassLoader();
+        ClassLoader classLoader = AppInfo.class.getClassLoader();
         InputStream inputStream = classLoader.getResourceAsStream(fileName);
         if (inputStream != null) {
             try {

+ 2 - 1
src/main/java/io/senslog/database/DBRepositoryPool.java

@@ -2,6 +2,7 @@ package io.senslog.database;
 
 import io.senslog.database.repository.AbstractRepository;
 import io.senslog.database.repository.ObservationRepository;
+import io.senslog.database.repository.PositionRepository;
 import io.senslog.database.repository.SensorRepository;
 
 import java.io.IOException;
@@ -24,8 +25,8 @@ public class DBRepositoryPool {
             throw new IOException("Can not connect to the database.");
         }
 
-
         REPOSITORIES.put(ObservationRepository.class, new ObservationRepository(connection));
+        REPOSITORIES.put(PositionRepository.class, new PositionRepository(connection));
         REPOSITORIES.put(SensorRepository.class, new SensorRepository(connection));
     }
 

+ 26 - 0
src/main/java/io/senslog/database/model/DBObject.java

@@ -0,0 +1,26 @@
+package io.senslog.database.model;
+
+public abstract class DBObject {
+
+    private long id;
+
+    protected DBObject(long id) {
+        this.id = id;
+    }
+
+    protected DBObject() {
+        this.id = -1;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public boolean isPersistence() {
+        return id >= 0;
+    }
+}

+ 42 - 0
src/main/java/io/senslog/database/model/DBObservation.java

@@ -0,0 +1,42 @@
+package io.senslog.database.model;
+
+import java.time.OffsetDateTime;
+
+public class DBObservation extends DBObject {
+
+    private final long unitId;
+    private final long sensorId;
+    private final double value;
+    private final OffsetDateTime timestamp;
+
+    public DBObservation(long unitId, long sensorId, double value, OffsetDateTime timestamp) {
+        this.unitId = unitId;
+        this.sensorId = sensorId;
+        this.value = value;
+        this.timestamp = timestamp;
+    }
+
+    public DBObservation(long id, long unitId, long sensorId, double value, int gid,  OffsetDateTime timestamp) {
+        super(id);
+        this.unitId = unitId;
+        this.sensorId = sensorId;
+        this.value = value;
+        this.timestamp = timestamp;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public long getSensorId() {
+        return sensorId;
+    }
+
+    public double getValue() {
+        return value;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+}

+ 75 - 0
src/main/java/io/senslog/database/model/DBPosition.java

@@ -0,0 +1,75 @@
+package io.senslog.database.model;
+
+import io.senslog.domain.Latitude;
+import io.senslog.domain.Longitude;
+
+import java.time.OffsetDateTime;
+
+public class DBPosition extends DBObject {
+
+    private final long unitId;
+    private final Latitude latitude;
+    private final Longitude longitude;
+    private final OffsetDateTime timestamp;
+
+    private final double altitude;
+    private final double speed;
+    private final double dop;
+
+    private final int srid;
+
+    public DBPosition(long id, long unitId, Latitude latitude, Longitude longitude, OffsetDateTime timestamp, double altitude, double speed, double dop, int srid) {
+        super(id);
+        this.unitId = unitId;
+        this.latitude = latitude;
+        this.longitude = longitude;
+        this.timestamp = timestamp;
+        this.altitude = altitude;
+        this.speed = speed;
+        this.dop = dop;
+        this.srid = srid;
+    }
+
+    public DBPosition(long unitId, Latitude latitude, Longitude longitude, OffsetDateTime timestamp, double altitude, double speed, double dop, int srid) {
+        this.unitId = unitId;
+        this.latitude = latitude;
+        this.longitude = longitude;
+        this.timestamp = timestamp;
+        this.altitude = altitude;
+        this.speed = speed;
+        this.dop = dop;
+        this.srid = srid;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public Latitude getLatitude() {
+        return latitude;
+    }
+
+    public Longitude getLongitude() {
+        return longitude;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public double getSpeed() {
+        return speed;
+    }
+
+    public double getAltitude() {
+        return altitude;
+    }
+
+    public double getDop() {
+        return dop;
+    }
+
+    public int getSrid() {
+        return srid;
+    }
+}

+ 142 - 8
src/main/java/io/senslog/database/repository/ObservationRepository.java

@@ -2,26 +2,160 @@ package io.senslog.database.repository;
 
 import io.senslog.database.DBConnection;
 import io.senslog.database.DBException;
-import cz.hsrs.db.model.Observation;
+import io.senslog.database.model.DBObservation;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static java.time.format.DateTimeFormatter.ofPattern;
 
 public class ObservationRepository implements AbstractRepository {
 
+    private static final DateTimeFormatter TIMESTAMP_FORMATTER = ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSX");
+
     private final DBConnection connection;
 
     public ObservationRepository(DBConnection connection) {
         this.connection = connection;
     }
 
-    public long insert(Observation observation) throws DBException {
+    public boolean insert(DBObservation observation) throws DBException {
+        if (observation == null) {
+            throw new DBException("Nothing to insert. Observation is empty.");
+        }
+        try {
+            connection.get().<Exception>useHandle(h -> h.inTransaction(t -> t.createUpdate(
+                    "INSERT INTO observations(time_stamp, observed_value, sensor_id, unit_id) " +
+                            "VALUES (:timestamp, :value, :sensorId, :unitId)"
+            )
+                    .bind("timestamp", observation.getTimestamp())
+                    .bind("value", observation.getValue())
+                    .bind("sensorId", observation.getSensorId())
+                    .bind("unitId", observation.getUnitId())
+                    .execute()));
+            return true; // TODO return observationId in future
+        } catch (Exception e) {
+            throw new DBException(e.getMessage());
+        }
+    }
+
+    public boolean insert(List<DBObservation> observations) throws DBException {
+        if (observations == null || observations.isEmpty()) {
+            throw new DBException("Nothing to insert. List of observation is empty.");
+        }
+        return false;
+    }
+
+    public List<DBObservation> selectLastObservationsBy(String groupName) throws DBException {
+        if (groupName == null || groupName.isEmpty()) {
+            throw new DBException("Name of group was not specified.");
+        }
+        try {
+            return connection.get().<List<DBObservation>, Exception>withHandle(h -> h.createQuery(
+                    "SELECT observation_id AS id, TO_CHAR(time_stamp, 'yyyy-MM-dd HH:mm:ss.SSSSSSOF') AS timestamp, " +
+                            "gid AS gid, observed_value AS value, o.sensor_id AS sensorId, o.unit_id AS unitId " +
+                            "FROM groups g, units_to_groups utg, units_to_sensors uts " +
+                            "LEFT JOIN observations o ON uts.last_obs = o.time_stamp " +
+                            "WHERE g.group_name = :group " +
+                            "AND g.id = utg.group_id " +
+                            "AND utg.unit_id = uts.unit_id " +
+                            "AND uts.unit_id = o.unit_id " +
+                            "AND uts.sensor_id = o.sensor_id " +
+                            "ORDER BY uts.unit_id, uts.sensor_id"
+            )
+                    .bind("group", groupName)
+                    .map((rs, ctx) -> new DBObservation(
+                                rs.getLong("id"),
+                                rs.getLong("unitId"),
+                                rs.getLong("sensorId"),
+                                rs.getLong("value"),
+                                rs.getInt("gid"),
+                                OffsetDateTime.parse(rs.getString("timestamp"), TIMESTAMP_FORMATTER)
+                            )
+                    ).list());
+        } catch (Exception e) {
+            throw new DBException(e.getMessage());
+        }
+    }
+
+    public List<DBObservation> selectLastObservationsBy(String groupName, long sensorId) throws DBException {
+        if (groupName == null || groupName.isEmpty()) {
+            throw new DBException("Name of group was not specified.");
+        }
+        try {
+            return connection.get().<List<DBObservation>, Exception>withHandle(h -> h.createQuery(
+                    "SELECT observation_id AS id, TO_CHAR(time_stamp, 'yyyy-MM-dd HH:mm:ss.SSSSSSOF') AS timestamp, " +
+                            "gid AS gid, observed_value AS value, o.unit_id AS unitId " +
+                            "FROM groups g, units_to_groups utg, units_to_sensors uts " +
+                            "LEFT JOIN observations o ON uts.last_obs = o.time_stamp " +
+                            "WHERE g.group_name = :group " +
+                            "AND g.id = utg.group_id " +
+                            "AND utg.unit_id = uts.unit_id " +
+                            "AND uts.sensor_id = :sensorId " +
+                            "AND uts.unit_id = o.unit_id " +
+                            "AND uts.sensor_id = o.sensor_id " +
+                            "ORDER BY uts.unit_id, uts.sensor_id"
+            )
+                    .bind("group", groupName)
+                    .bind("sensorId", sensorId)
+                    .map((rs, ctx) -> new DBObservation(
+                                    rs.getLong("id"),
+                                    rs.getLong("unitId"),
+                                    sensorId,
+                                    rs.getLong("value"),
+                                    rs.getInt("gid"),
+                                    OffsetDateTime.parse(rs.getString("timestamp"), TIMESTAMP_FORMATTER)
+                            )
+                    ).list());
+        } catch (Exception e) {
+            throw new DBException(e.getMessage());
+        }
+    }
+
+    public List<DBObservation> selectLastObservationsBy(long unitId) throws DBException {
+        try {
+            return connection.get().<List<DBObservation>, Exception>withHandle(h -> h.createQuery(
+                    "SELECT observation_id AS id, TO_CHAR(time_stamp, 'yyyy-MM-dd HH:mm:ss.SSSSSSOF') AS timestamp, " +
+                            "gid AS gid, observed_value AS value, o.sensor_id AS sensorId " +
+                            "FROM units_to_sensors uts " +
+                            "LEFT JOIN observations o ON uts.last_obs = o.time_stamp " +
+                            "WHERE uts.unit_id = :unitId AND uts.sensor_id = o.sensor_id"
+            )
+                    .bind("unitId", unitId)
+                    .map((rs, ctx) -> new DBObservation(
+                            rs.getLong("id"),
+                            unitId,
+                            rs.getLong("sensorId"),
+                            rs.getLong("value"),
+                            rs.getInt("gid"),
+                            OffsetDateTime.parse(rs.getString("timestamp"), TIMESTAMP_FORMATTER))
+                    ).list());
+        } catch (Exception e) {
+            throw new DBException(e.getMessage());
+        }
+    }
+
+    public List<DBObservation> selectLastObservationsBy(long unitId, long sensorId) throws DBException {
         try {
-            connection.get().<Exception>useHandle(h -> h.inTransaction(t -> t.execute(
-                    "INSERT INTO observations(time_stamp, observed_value, sensor_id, unit_id) VALUES (?, ?, ?, ?)",
-                    observation.getTimeStamp(), observation.getObservedValue(), observation.getSensorId(), observation.getUnitId()
-                    )
-            ));
+            return connection.get().<List<DBObservation>, Exception>withHandle(h -> h.createQuery(
+                    "SELECT observation_id AS id, TO_CHAR(time_stamp, 'yyyy-MM-dd HH:mm:ss.SSSSSSOF') AS timestamp, " +
+                            "gid AS gid, observed_value AS value " +
+                            "FROM units_to_sensors uts "
+                            + "LEFT JOIN observations o ON uts.last_obs = o.time_stamp "
+                            + "WHERE uts.unit_id = :unitId AND uts.sensor_id = :sensorId AND uts.sensor_id = o.sensor_id"
+            )
+                    .bind("unitId", unitId)
+                    .bind("sensorId", sensorId)
+                    .map((rs, ctx) -> new DBObservation(
+                            rs.getLong("id"),
+                            unitId, sensorId,
+                            rs.getLong("value"),
+                            rs.getInt("gid"),
+                            OffsetDateTime.parse(rs.getString("timestamp"), TIMESTAMP_FORMATTER))
+                    ).list());
         } catch (Exception e) {
             throw new DBException(e.getMessage());
         }
-        return 0L; // observation_id
     }
 }

+ 38 - 0
src/main/java/io/senslog/database/repository/PositionRepository.java

@@ -0,0 +1,38 @@
+package io.senslog.database.repository;
+
+import io.senslog.database.DBConnection;
+import io.senslog.database.DBException;
+import io.senslog.database.model.DBObservation;
+import io.senslog.database.model.DBPosition;
+
+public class PositionRepository implements AbstractRepository {
+
+    private final DBConnection connection;
+
+    public PositionRepository(DBConnection connection) {
+        this.connection = connection;
+    }
+
+    public boolean insert(DBPosition position) throws DBException {
+        try {
+            connection.get().<Exception>useHandle(h -> h.inTransaction(t -> t.createUpdate(
+                    "INSERT INTO units_positions(altitude, the_geom, unit_id, time_stamp, gid, speed, dop) " +
+                            "VALUES (:alt, st_geomfromtext(concat('POINT(', :lon, ' ', :lat, ')'), :srid), :unitId, :timestamp, " +
+                            "nextval('units_positions_gid_seq'::regclass), :speed, :dop)"
+            )
+                    .bind("alt", position.getAltitude())
+                    .bind("lon", position.getLongitude().get())
+                    .bind("lat", position.getLatitude().get())
+                    .bind("srid", position.getSrid())
+                    .bind("unitId", position.getUnitId())
+                    .bind("timestamp", position.getTimestamp())
+                    .bind("speed", position.getSpeed())
+                    .bind("dop", position.getDop())
+                    .execute()
+            ));
+            return true; // TODO return 'gid' in future
+        } catch (Exception e) {
+            throw new DBException(e.getMessage());
+        }
+    }
+}

+ 37 - 0
src/main/java/io/senslog/domain/Latitude.java

@@ -0,0 +1,37 @@
+package io.senslog.domain;
+
+import java.util.Objects;
+
+public class Latitude {
+
+    private final double value;
+
+    public Latitude(double longitude) {
+        if (longitude < -90 || longitude > 90) {
+            throw new IllegalArgumentException("Latitude is not in range of -90 to 90.");
+        }
+        this.value = longitude;
+    }
+
+    public double get() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Latitude latitude = (Latitude) o;
+        return Double.compare(latitude.value, value) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    @Override
+    public String toString() {
+        return Double.toString(value);
+    }
+}

+ 37 - 0
src/main/java/io/senslog/domain/Longitude.java

@@ -0,0 +1,37 @@
+package io.senslog.domain;
+
+import java.util.Objects;
+
+public class Longitude {
+
+    private final double value;
+
+    public Longitude(double longitude) {
+        if (longitude < -180 || longitude > 180) {
+            throw new IllegalArgumentException("Longitude is not in range of -180 to 180.");
+        }
+        this.value = longitude;
+    }
+
+    public double get() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Longitude longitude = (Longitude) o;
+        return Double.compare(longitude.value, value) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    @Override
+    public String toString() {
+        return Double.toString(value);
+    }
+}

+ 0 - 49
src/main/java/io/senslog/manager/ObservationManager.java

@@ -1,49 +0,0 @@
-package io.senslog.manager;
-
-import io.senslog.database.DBException;
-import io.senslog.database.DBRepositoryPool;
-import io.senslog.database.repository.ObservationRepository;
-import io.senslog.database.repository.SensorRepository;
-import cz.hsrs.db.model.Observation;
-
-import java.io.IOException;
-
-import static cz.hsrs.track.TrackIgnitionSolver.IGNITION_SENSOR_ID;
-
-public class ObservationManager {
-
-    private final ObservationRepository observationRep;
-    private final SensorRepository unitRep;
-
-    public static ObservationManager newInstance() {
-        try {
-            ObservationRepository obsRep = DBRepositoryPool.getRepository(ObservationRepository.class);
-            SensorRepository seRep = DBRepositoryPool.getRepository(SensorRepository.class);
-            return new ObservationManager(obsRep, seRep);
-        } catch (IOException e) {
-            throw new IllegalStateException(e.getMessage());
-        }
-    }
-
-    public ObservationManager(ObservationRepository observationRep, SensorRepository unitRep) {
-        this.observationRep = observationRep;
-        this.unitRep = unitRep;
-    }
-
-    public void save(Observation observation) {
-
-        if (observation.getSensorId() == IGNITION_SENSOR_ID) {
-            // TODO solve
-        }
-
-        try {
-            long obsId = observationRep.insert(observation);
-            if (obsId <= 0) {
-                throw new RuntimeException("Observation was not inserted successfully.");
-            }
-        } catch (DBException e) {
-            throw new RuntimeException(e.getMessage());
-        }
-    }
-
-}

+ 117 - 0
src/main/java/io/senslog/service/DataService.java

@@ -0,0 +1,117 @@
+package io.senslog.service;
+
+import io.senslog.database.DBException;
+import io.senslog.database.DBRepositoryPool;
+import io.senslog.database.model.DBObservation;
+import io.senslog.database.model.DBPosition;
+import io.senslog.database.repository.ObservationRepository;
+import io.senslog.database.repository.PositionRepository;
+import io.senslog.database.repository.SensorRepository;
+import io.senslog.ws.WSException;
+import io.senslog.ws.model.WSObservation;
+import io.senslog.ws.model.WSPosition;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static cz.hsrs.track.TrackIgnitionSolver.IGNITION_SENSOR_ID;
+
+public class DataService {
+
+    // TODO where to get this value?
+    private static final boolean USE_TRACKS = false;
+
+    private final ObservationRepository observationRep;
+    private final PositionRepository positionRep;
+    private final SensorRepository unitRep;
+
+    public static DataService newInstance() {
+        try {
+            ObservationRepository obsRep = DBRepositoryPool.getRepository(ObservationRepository.class);
+            PositionRepository posRep = DBRepositoryPool.getRepository(PositionRepository.class);
+            SensorRepository seRep = DBRepositoryPool.getRepository(SensorRepository.class);
+            return new DataService(obsRep, posRep, seRep);
+        } catch (IOException e) {
+            throw new IllegalStateException(e.getMessage());
+        }
+    }
+
+    public DataService(
+            ObservationRepository observationRep,
+            PositionRepository positionRep,
+            SensorRepository unitRep
+    ) {
+        this.observationRep = observationRep;
+        this.positionRep = positionRep;
+        this.unitRep = unitRep;
+    }
+
+    public boolean saveObservation(WSObservation wsObservation) {
+
+        if (wsObservation.getSensorId() == IGNITION_SENSOR_ID) {
+            // TODO solve
+        }
+
+        DBObservation dbObservation = new DBObservation(
+                wsObservation.getUnitId(), wsObservation.getSensorId(),
+                wsObservation.getValue(), wsObservation.getTimestamp()
+        );
+
+        try {
+            return observationRep.insert(dbObservation);
+        } catch (DBException e) {
+            throw new WSException(500, e.getMessage());
+        }
+    }
+
+    public boolean savePosition(WSPosition wsPosition) {
+
+        if (USE_TRACKS) {
+            // TODO solve(wsPosition);
+        }
+
+
+        DBPosition dbPosition = new DBPosition(
+                wsPosition.getUnitId(), wsPosition.getLatitude(), wsPosition.getLongitude(),
+                wsPosition.getTimestamp(), wsPosition.getAltitude(), wsPosition.getSpeed(),
+                wsPosition.getDop(), 4326
+        );
+
+        try {
+            return positionRep.insert(dbPosition);
+        } catch (DBException e) {
+            throw new WSException(500, e.getMessage());
+        }
+    }
+
+    public List<WSObservation> loadLastObservations(String groupName, Long unitId, Long sensorId) {
+        List<DBObservation> observations;
+        try {
+            if (groupName != null) {
+                if (sensorId != null) {
+                    observations = observationRep.selectLastObservationsBy(groupName, sensorId);
+                } else {
+                    observations = observationRep.selectLastObservationsBy(groupName);
+                }
+            } else if (unitId != null) {
+                if (sensorId != null) {
+                    observations = observationRep.selectLastObservationsBy(unitId, sensorId);
+                } else {
+                    observations = observationRep.selectLastObservationsBy(unitId);
+                }
+            } else {
+                observations = Collections.emptyList();
+            }
+
+            List<WSObservation> result = new ArrayList<>(observations.size());
+            for (DBObservation dbO : observations) {
+                result.add(new WSObservation(dbO.getUnitId(), dbO.getSensorId(), dbO.getValue(), dbO.getTimestamp()));
+            }
+            return result;
+        } catch (DBException e) {
+            throw new WSException(500, e.getMessage());
+        }
+    }
+}

+ 28 - 0
src/main/java/io/senslog/util/NumberUtils.java

@@ -0,0 +1,28 @@
+package io.senslog.util;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public final class NumberUtils {
+
+    private NumberUtils() {}
+
+    private static <T> Optional<T> tryParse(String value, Function<String, T> fnc) {
+        if (org.apache.commons.lang3.math.NumberUtils.isParsable(value)) {
+            return Optional.of(fnc.apply(value));
+        }
+        return Optional.empty();
+    }
+
+    public static Optional<Double> tryParseDouble(String value) {
+        return tryParse(value, Double::parseDouble);
+    }
+
+    public static Optional<Long> tryParseLong(String value) {
+        return tryParse(value, Long::parseLong);
+    }
+
+    public static Optional<Integer> tryParseInt(String value) {
+        return tryParse(value, Integer::parseInt);
+    }
+}

+ 18 - 0
src/main/java/io/senslog/util/TimestampUtils.java

@@ -0,0 +1,18 @@
+package io.senslog.util;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+
+public final class TimestampUtils {
+
+    private TimestampUtils() {}
+
+    public static Optional<OffsetDateTime> tryParse(String value) {
+        if (value == null || value.isEmpty()) {
+            return Optional.empty();
+        }
+        final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX");
+        return Optional.ofNullable(OffsetDateTime.parse(value, formatter));
+    }
+}

+ 40 - 0
src/main/java/io/senslog/ws/WSException.java

@@ -0,0 +1,40 @@
+package io.senslog.ws;
+
+import java.util.function.Function;
+
+public class WSException extends RuntimeException {
+
+    @FunctionalInterface
+    public interface ThrowingWrap<P, R> extends Function<P, R> {
+
+        @Override
+        default R apply(P p) {
+            try {
+                return acceptThrows(p);
+            } catch (Exception e) {
+                throw new WSException(400, e.getMessage());
+            }
+        }
+
+        R acceptThrows(P val) throws Exception;
+    }
+
+    public static <T, R> Function<T, R> wrap(ThrowingWrap<T, R> function) {
+        return function;
+    }
+
+    private final int code;
+
+    public WSException(int code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public WSException(int code, String pattern, Object... params) {
+        this(code, String.format(pattern, params));
+    }
+
+    public int getCode() {
+        return code;
+    }
+}

+ 21 - 0
src/main/java/io/senslog/ws/handler/ExceptionHandler.java

@@ -0,0 +1,21 @@
+package io.senslog.ws.handler;
+
+import io.senslog.ws.WSException;
+import net.sf.json.JSONObject;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Provider
+public class ExceptionHandler implements ExceptionMapper<WSException> {
+
+    @Override
+    public Response toResponse(WSException exception) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("timestamp", System.currentTimeMillis());
+        jsonObject.put("message", exception.getMessage());
+        return Response.status(exception.getCode())
+                .entity(jsonObject.toString()).build();
+    }
+}

+ 0 - 44
src/main/java/io/senslog/ws/handler/FeederHandler.java

@@ -1,44 +0,0 @@
-package io.senslog.ws.handler;
-
-import io.senslog.manager.ObservationManager;
-
-import javax.ws.rs.*;
-import javax.ws.rs.core.Response;
-
-import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
-
-@Path("/v1/FeederServlet")
-public class FeederHandler {
-
-    private final ObservationManager observationManager;
-
-    public FeederHandler() {
-        this.observationManager = ObservationManager.newInstance();
-    }
-
-    @POST @Produces(APPLICATION_JSON)
-    public Response handlePostOperation(
-            @QueryParam("Operation") String operationType,
-            @QueryParam("value")     String value,
-            @QueryParam("date")      String timestamp,
-            @QueryParam("unit_id")   String unitId,
-            @QueryParam("sensor_id") String sensorId
-    ) {
-        return Response.ok().build();
-    }
-
-    @GET @Produces(APPLICATION_JSON)
-    public Response handleGetOperation(
-            @QueryParam("Operation") String operationType,
-            @QueryParam("value")     String value,
-            @QueryParam("date")      String timestamp,
-            @QueryParam("unit_id")   String unitId,
-            @QueryParam("sensor_id") String sensorId
-    ) {
-        return Response.ok().build();
-    }
-
-    private Response insertObservation() {
-        return null;
-    }
-}

+ 7 - 10
src/main/java/cz/hsrs/rest/provider/InfoRest.java → src/main/java/io/senslog/ws/handler/InfoHandler.java

@@ -1,6 +1,6 @@
-package cz.hsrs.rest.provider;
+package io.senslog.ws.handler;
 
-import io.senslog.core.ApplicationInfo;
+import io.senslog.core.AppInfo;
 import net.sf.json.JSONObject;
 
 import javax.ws.rs.GET;
@@ -9,16 +9,16 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.Response;
 import java.util.concurrent.TimeUnit;
 
-import static io.senslog.core.ApplicationInfo.appVersion;
-import static io.senslog.core.ApplicationInfo.buildVersion;
+import static io.senslog.core.AppInfo.appVersion;
+import static io.senslog.core.AppInfo.buildVersion;
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
 @Path("/info")
-public class InfoRest {
+public class InfoHandler {
 
     @GET @Produces(APPLICATION_JSON)
     public Response getOverallInfo() {
-        long uptimeMillis = ApplicationInfo.uptime();
+        long uptimeMillis = AppInfo.uptime();
         String uptime = String.format("%02d min %02d sec",
                 TimeUnit.MILLISECONDS.toMinutes(uptimeMillis),
                 TimeUnit.MILLISECONDS.toSeconds(uptimeMillis) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(uptimeMillis))
@@ -28,9 +28,6 @@ public class InfoRest {
         jsonObject.put("appVersion", appVersion());
         jsonObject.put("buildVersion", buildVersion());
 
-        return Response
-                .status(200)
-                .entity(jsonObject.toString())
-                .build();
+        return Response.ok(jsonObject.toString()).build();
     }
 }

+ 90 - 0
src/main/java/io/senslog/ws/handler/v1/DataPublishingHandler.java

@@ -0,0 +1,90 @@
+package io.senslog.ws.handler.v1;
+
+import io.senslog.service.DataService;
+import io.senslog.util.NumberUtils;
+import io.senslog.ws.WSException;
+import io.senslog.ws.model.WSObservation;
+import net.sf.json.JSONArray;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Path("/v1")
+public class DataPublishingHandler {
+
+    private final DataService dataService;
+
+    private final Map<String, Function<MultivaluedMap<String, String>, Response>> operationFnc;
+    private static final Function<MultivaluedMap<String, String>, Response> DEFAULT_FNC;
+
+    static {
+        DEFAULT_FNC = values -> {
+            throw new WSException(406, "Unsupported operation '%s'.", values.getFirst("Operation"));
+        };
+    }
+
+    public DataPublishingHandler() {
+        this.dataService = DataService.newInstance();
+
+        operationFnc = new HashMap<>();
+        operationFnc.put("GetLastObservations", this::loadLastObservations);
+        operationFnc.put("GetSensors",          this::loadAllSensors);
+        operationFnc.put("GetObservations",     this::loadAllObservations);
+    }
+
+    @Path("/SensorService")
+    @GET @Produces(APPLICATION_JSON)
+    public Response handleGetOperation(@QueryParam("Operation") @DefaultValue("") String operationType, @Context UriInfo allUri) {
+        return operationFnc.getOrDefault(operationType, DEFAULT_FNC).apply(allUri.getQueryParameters());
+    }
+
+
+    private Response loadLastObservations(MultivaluedMap<String, String> params) {
+        String groupName = params.getFirst("group");
+        String unitIdStr = params.getFirst("unit_id");
+
+        boolean isGroupName = StringUtils.isNoneBlank(groupName);
+        boolean isUnitId = StringUtils.isNotBlank(unitIdStr);
+
+        if (!isGroupName && !isUnitId) {
+            throw new WSException(400, "One of the parameters [group, unit_id] is required.");
+        }
+        if (isGroupName && isUnitId) {
+            throw new WSException(400, "Unsupported combination of parameters 'group' and 'unit_id'.");
+        }
+
+        String sensorIdStr = params.getFirst("sensor_id");
+        boolean isSensorId = StringUtils.isNoneBlank(sensorIdStr);
+        Long sensorId = isSensorId ? NumberUtils.tryParseLong(params.getFirst("sensor_id"))
+                .orElseThrow(() -> new WSException(400, "Parameter 'sensor_id' is not a number.")) : null;
+
+        Long unitId = isUnitId ? NumberUtils.tryParseLong(unitIdStr)
+                .orElseThrow(() -> new WSException(400, "Parameter 'unit_id' is not a number.")) : null;
+
+        List<WSObservation> resultObservations = dataService.loadLastObservations(groupName, unitId, sensorId);
+
+        JSONArray jsonResult = new JSONArray();
+        resultObservations.forEach(o -> jsonResult.add(o.toJson()));
+
+        return Response.ok(jsonResult.toString()).build();
+    }
+
+    private Response loadAllSensors(MultivaluedMap<String, String> params) {
+        throw new WSException(425, "Operation has not been implemented yet.");
+    }
+
+    private Response loadAllObservations(MultivaluedMap<String, String> params) {
+        throw new WSException(425, "Operation has not been implemented yet.");
+    }
+}

+ 83 - 0
src/main/java/io/senslog/ws/handler/v1/DataRetrievingHandler.java

@@ -0,0 +1,83 @@
+package io.senslog.ws.handler.v1;
+
+import io.senslog.service.DataService;
+import io.senslog.ws.WSException;
+import io.senslog.ws.model.WSObservation;
+import io.senslog.ws.model.WSPosition;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+
+@Path("/v1")
+public class DataRetrievingHandler {
+
+    private final DataService dataService;
+
+    private final Map<String, Function<MultivaluedMap<String, String>, Response>> operationFnc;
+    private static final Function<MultivaluedMap<String, String>, Response> DEFAULT_FNC;
+
+    static {
+        DEFAULT_FNC = values -> {
+            throw new WSException(406, "Unsupported operation '%s'.", values.getFirst("Operation"));
+        };
+    }
+
+    public DataRetrievingHandler() {
+        this.dataService = DataService.newInstance();
+
+        operationFnc = new HashMap<>();
+        operationFnc.put("InsertObservation", this::saveNewObservation);
+        operationFnc.put("InsertPosition",    this::saveNewPosition);
+        operationFnc.put("InsertAlertEvent",  this::saveNewAlertEvent);
+        operationFnc.put("SolvingAlertEvent", this::solvingAlertEvent);
+    }
+
+    @Path("/FeederServlet")
+    @POST @Produces(TEXT_PLAIN)
+    public Response handlePostOperation(@QueryParam("Operation") @DefaultValue("") String operationType, @Context UriInfo allUri) {
+        return operationFnc.getOrDefault(operationType, DEFAULT_FNC).apply(allUri.getQueryParameters());
+    }
+
+    @Path("/FeederServlet")
+    @GET @Produces(TEXT_PLAIN)
+    public Response handleGetOperation(@QueryParam("Operation") @DefaultValue("") String operationType, @Context UriInfo allUri) {
+        return operationFnc.getOrDefault(operationType, DEFAULT_FNC).apply(allUri.getQueryParameters());
+    }
+
+    private Response saveNewObservation(MultivaluedMap<String, String> params) {
+        WSObservation observation = WSObservation.parse(
+                params.getFirst("unit_id"), params.getFirst("sensor_id"),
+                params.getFirst("value"), params.getFirst("date")
+        );
+        boolean result = dataService.saveObservation(observation);
+        return Response.ok(Boolean.toString(result)).build();
+    }
+
+    private Response saveNewPosition(MultivaluedMap<String, String> params) {
+        WSPosition position = WSPosition.parse(
+                params.getFirst("unit_id"), params.getFirst("lat"),
+                params.getFirst("lon"), params.getFirst("alt"),
+                params.getFirst("speed"), params.getFirst("dop"),
+                params.getFirst("date")
+        );
+        boolean result = dataService.savePosition(position);
+        return Response.ok(Boolean.toString(result)).build();
+    }
+
+    private Response saveNewAlertEvent(MultivaluedMap<String, String> params) {
+        throw new WSException(425, "Operation has not been implemented yet.");
+    }
+
+    private Response solvingAlertEvent(MultivaluedMap<String, String> params) {
+        throw new WSException(425, "Operation has not been implemented yet.");
+    }
+}

+ 6 - 0
src/main/java/io/senslog/ws/model/WSObject.java

@@ -0,0 +1,6 @@
+package io.senslog.ws.model;
+
+public interface WSObject {
+
+    String toJson();
+}

+ 68 - 0
src/main/java/io/senslog/ws/model/WSObservation.java

@@ -0,0 +1,68 @@
+package io.senslog.ws.model;
+
+import io.senslog.util.NumberUtils;
+import io.senslog.util.TimestampUtils;
+import io.senslog.ws.WSException;
+import net.sf.json.JSONObject;
+
+import java.time.OffsetDateTime;
+
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class WSObservation implements WSObject {
+
+    private final long unitId;
+    private final long sensorId;
+    private final double value;
+    private final OffsetDateTime timestamp;
+
+    public static WSObservation parse(String unitIdStr, String sensorIdStr, String valueStr, String timestampStr) {
+        double value = NumberUtils.tryParseDouble(valueStr)
+                .orElseThrow(() -> new WSException(400, "Param 'value' is not a number."));
+
+        long unitId = NumberUtils.tryParseLong(unitIdStr)
+                .orElseThrow(() -> new WSException(400, "Param 'unit_id' is not a number."));
+
+        long sensorId = NumberUtils.tryParseLong(sensorIdStr)
+                .orElseThrow(() -> new WSException(400, "Param 'sensor_id' is not a number."));
+
+        OffsetDateTime timestamp = TimestampUtils.tryParse(timestampStr)
+                .filter(val -> val.isBefore(OffsetDateTime.now()))
+                .orElseThrow(() -> new WSException(400, "Param 'date' is not in correct format or it is in future."));
+
+        return new WSObservation(unitId, sensorId, value, timestamp);
+    }
+
+    public WSObservation(long unitId, long sensorId, double value, OffsetDateTime timestamp) {
+        this.unitId = unitId;
+        this.sensorId = sensorId;
+        this.value = value;
+        this.timestamp = timestamp;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public long getSensorId() {
+        return sensorId;
+    }
+
+    public double getValue() {
+        return value;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public String toJson() {
+        JSONObject json = new JSONObject();
+        json.put("unit_id", unitId);
+        json.put("sensor_id", sensorId);
+        json.put("value", value);
+        json.put("time_stamp", timestamp.format(ofPattern("yyyy-MM-dd HH:mm:ssX")));
+        return json.toString();
+    }
+}

+ 113 - 0
src/main/java/io/senslog/ws/model/WSPosition.java

@@ -0,0 +1,113 @@
+package io.senslog.ws.model;
+
+import io.senslog.domain.Latitude;
+import io.senslog.domain.Longitude;
+import io.senslog.util.NumberUtils;
+import io.senslog.util.TimestampUtils;
+import io.senslog.ws.WSException;
+import net.sf.json.JSONObject;
+
+import java.time.OffsetDateTime;
+
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class WSPosition implements WSObject  {
+
+    // mandatory
+    private final long unitId;
+    private final Latitude latitude;      // -90 to 90
+    private final Longitude longitude;     // -180 to 180
+    private final OffsetDateTime timestamp; // <= now()
+
+    // optional
+    private final double altitude;
+    private final double speed;
+    private final double dop;
+
+    public static WSPosition parse(
+            String unitIdStr, String latitudeStr, String longitudeStr,
+            String altitudeStr, String speedStr, String dopStr, String timestampStr
+    ) {
+        long unitId = NumberUtils.tryParseLong(unitIdStr)
+                .orElseThrow(() -> new WSException(400, "Param 'unit_id' is not a number."));
+
+        Latitude latitude = NumberUtils.tryParseDouble(latitudeStr)
+                .map(WSException.wrap(Latitude::new))
+                .orElseThrow(() -> new WSException(400, "Param 'lat' is not a number or in wrong range."));
+
+        Longitude longitude = NumberUtils.tryParseDouble(longitudeStr)
+                .map(WSException.wrap(Longitude::new))
+                .orElseThrow(() -> new WSException(400, "Param 'lon' is not a number."));
+
+        OffsetDateTime timestamp = TimestampUtils.tryParse(timestampStr)
+                .filter(val -> val.isBefore(OffsetDateTime.now()))
+                .orElseThrow(() -> new WSException(400, "Param 'date' is not in correct format or it is in future."));
+
+        double altitude = NumberUtils.tryParseDouble(altitudeStr).orElse(Double.NaN);
+        double speed = NumberUtils.tryParseDouble(speedStr).orElse(Double.NaN);
+        double dop = NumberUtils.tryParseDouble(dopStr).filter(val -> val > 0).orElse(Double.NaN);
+
+        return new WSPosition(unitId, latitude, longitude, altitude, speed, dop, timestamp);
+    }
+
+    public WSPosition(long unitId, Latitude latitude, Longitude longitude,
+                      double altitude, double speed, double dop, OffsetDateTime timestamp
+    ) {
+        this.unitId = unitId;
+        this.latitude = latitude;
+        this.longitude = longitude;
+        this.timestamp = timestamp;
+        this.altitude = altitude;
+        this.speed = speed;
+        this.dop = dop;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public Latitude getLatitude() {
+        return latitude;
+    }
+
+    public Longitude getLongitude() {
+        return longitude;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public double getAltitude() {
+        return altitude;
+    }
+
+    public double getSpeed() {
+        return speed;
+    }
+
+    public double getDop() {
+        return dop;
+    }
+
+    @Override
+    public String toJson() {
+        JSONObject json = new JSONObject();
+
+        json.put("unit_id", unitId);
+        json.put("latitude", latitude.get());
+        json.put("longitude", longitude.get());
+        json.put("time_stamp", timestamp.format(ofPattern("yyyy-MM-dd HH:mm:ssX")));
+        if (!Double.isNaN(altitude)) {
+            json.put("altitude", altitude);
+        }
+        if (!Double.isNaN(speed)) {
+            json.put("speed", speed);
+        }
+        if (dop > 0) {
+            json.put("dop", dop);
+        }
+
+        return json.toString();
+    }
+}

+ 4 - 6
src/main/webapp/WEB-INF/database.properties

@@ -20,15 +20,13 @@ UnitsPositions_table= mercator_positions
 #Password = map.LOG_16
 
 # FIE20 - tunnel
-Address=jdbc:postgresql://localhost:5433/senslog1
+#Address=jdbc:postgresql://localhost:5433/senslog1
 # FIE20 - local
-#Address=jdbc:postgresql://localhost:5432/senslog1
-Username = senslog_app
-Password = SENSlog
+Address=jdbc:postgresql://localhost:5432/senslog1
+Username = postgres
+Password = root
 
 # picture for head of all pages (height ca. 75px)
 #Brand_picture = logo-OTN.png
 Brand_picture = logo-foodie.png
 #Brand_picture = logo-farmtelemetry.png
-
-Port = 5432

+ 29 - 8
src/main/webapp/WEB-INF/web.xml

@@ -111,7 +111,6 @@
 <!-- Jersey 2.x REST -->
   <servlet>
       <servlet-name>Jersey REST Service</servlet-name>
-      <!-- <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>-->
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
       <init-param>
         <!-- <param-name>com.sun.jersey.config.property.packages</param-name>-->
@@ -130,12 +129,6 @@
           <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
           <param-value>cz.hsrs.rest.util.CorsFilter</param-value>
       </init-param>
-<!--     <init-param> -->
-<!--       <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name> -->
-<!--       <param-value>com.sun.jersey.api.container.filter.LoggingFilter</param-value> -->
-<!--       <param-value>com.sun.jersey.api.container.filter.LoggingFilter;cz.hsrs.hsformserver.rest.AuthFilter</param-value> -->
-<!--       <param-value>cz.hsrs.hsformserver.rest.AuthFilter</param-value> -->
-<!--     </init-param> -->
       <load-on-startup>1</load-on-startup>
   </servlet>
 
@@ -208,5 +201,33 @@
     <session-config>
         <session-timeout>240</session-timeout>
     </session-config>
-
+
+<!--    NEW CONFIGURATION FOR 2.0 -->
+    <servlet>
+        <servlet-name>REST_API_V2</servlet-name>
+        <description>New version of SensLog API</description>
+        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>jersey.config.server.provider.packages</param-name>
+            <param-value>io.senslog.ws</param-value>
+        </init-param>
+        <init-param>
+            <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>jersey.config.server.provider.classnames</param-name>
+            <param-value>org.glassfish.jersey.media.multipart.MultiPartFeature;cz.hsrs.rest.util.CorsFilter</param-value>
+        </init-param>
+        <init-param>
+            <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
+            <param-value>cz.hsrs.rest.util.CorsFilter</param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>REST_API_V2</servlet-name>
+        <url-pattern>/api/*</url-pattern>
+    </servlet-mapping>
+
 </web-app>

+ 2 - 2
src/main/webapp/signin.jsp

@@ -2,7 +2,7 @@
     pageEncoding="UTF-8" %>
     <%@ page import = "cz.hsrs.servlet.security.*"%>
     <%@ page import = "cz.hsrs.db.pool.*"%>
-<%@ page import="io.senslog.core.ApplicationInfo" %>
+<%@ page import="io.senslog.core.AppInfo" %>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
     <head>
@@ -50,7 +50,7 @@
         <div style="clear:both"></div>
         <div id="footer">
             &copy; CCSS, <a href="http://www.ccss.cz/">http://www.ccss.cz/</a>
-         version: <%=ApplicationInfo.appVersion()%>  build: <%=ApplicationInfo.buildVersion()%>
+         version: <%=AppInfo.appVersion()%>  build: <%=AppInfo.buildVersion()%>
         </div>
     </body>
 </html>