فهرست منبع

Refactoring code

Lukas Cerny 4 سال پیش
والد
کامیت
ef64b29a36

+ 2 - 2
Dockerfile

@@ -11,7 +11,7 @@ COPY $module /app/$module
 COPY $config_file /app/config.yaml
 COPY gradle /app/gradle
 COPY build.gradle settings.gradle gradlew /app/
-WORKDIR /app/
+WORKDIR /app
 RUN ./gradlew assemble
 
 FROM adoptopenjdk/openjdk11:jdk-11.0.10_9-debian AS unitTest
@@ -26,8 +26,8 @@ RUN ./gradlew integrationTest
 
 FROM adoptopenjdk/openjdk11:jre-11.0.10_9-alpine AS production
 ENV port 8080
-WORKDIR /app
 COPY --from=builder /app/bin/libs /app/bin
 COPY --from=builder /app/config.yaml /app/
+WORKDIR /app
 EXPOSE $port
 CMD java -cp "bin/*" io.connector.Main -cf config.yaml -p $port

+ 33 - 33
README.md

@@ -1,34 +1,34 @@
-# Description
-
-A connector providing standardized interface for components outside the AFarCloud
-architecture and connecting internally to API of AFarCloud component will be designed and
-developed. The main goal of this OGC2AFC Connector is to receive and publish sensor
-data e.g. from general sensors, collars. The OGC2AFC Connector connects to the AFarCloud REST
-Server and transforms requests specified by the OGC SensorThings standard
-to the requests defined by the REST Server API.
-
-This software was created as a part of the ECSEL JU project 783221 Aggregate FARming in the CLOUD (AFarCloud).
-
-### Build
-
-Requirements: *Gradle*
-
-`./gradle build`
-
-### Run
-
-Requirements: *Java 8+*
-
-`java -cp "bin/libs/*" io.connector.Main -cf config/ogc2afc.yaml -p <port>`
-
-### Build docker image
-
-`docker build -t <image_custom_name> --build-arg config_file=<yaml_file> --build-arg port=<server_port> -f ./docker/Dockerfile .`
-
-### Run docker image
-
-`docker run --name <container_name> -p <host_port>:<server_port> <image>`
-
-### Usage
-
+# Description
+
+A connector providing standardized interface for components outside the AFarCloud
+architecture and connecting internally to API of AFarCloud component will be designed and
+developed. The main goal of this OGC2AFC Connector is to receive and publish sensor
+data e.g. from general sensors, collars. The OGC2AFC Connector connects to the AFarCloud REST
+Server and transforms requests specified by the OGC SensorThings standard
+to the requests defined by the REST Server API.
+
+This software was created as a part of the ECSEL JU project 783221 Aggregate FARming in the CLOUD (AFarCloud).
+
+### Build
+
+Requirements: *Gradle*
+
+`./gradle build`
+
+### Run
+
+Requirements: *Java 8+*
+
+`java -cp "bin/libs/*" io.connector.Main -cf config/ogc2afc.yaml -p <port>`
+
+### Build docker image
+
+<strike>`docker build -t <image_custom_name> --build-arg config_file=<yaml_file> --build-arg port=<server_port> -f ./docker/Dockerfile .`</strike>
+
+### Run docker image
+
+<strike>`docker run --name <container_name> -p <host_port>:<server_port> <image>`</strike>
+
+### Usage
+
 `http://www.<domain>:<port>/api/AFarCloud/OGCSensorThings/Things`

+ 40 - 0
connector-core/src/main/java/io/connector/core/util/Tuple.java

@@ -0,0 +1,40 @@
+package io.connector.core.util;
+
+import java.util.Objects;
+
+public class Tuple<A, B> {
+    private final A item1;
+    private final B item2;
+
+    public static <A, B> Tuple<A, B> of(A item1, B item2) {
+        return new Tuple<>(item1, item2);
+    }
+
+    private Tuple(A item1, B item2) {
+        this.item1 = item1;
+        this.item2 = item2;
+    }
+
+    public A getItem1() {
+        return this.item1;
+    }
+
+    public B getItem2() {
+        return this.item2;
+    }
+
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        } else if (o != null && this.getClass() == o.getClass()) {
+            Tuple<?, ?> tuple = (Tuple<?, ?>)o;
+            return this.item1.equals(tuple.item1) && this.item2.equals(tuple.item2);
+        } else {
+            return false;
+        }
+    }
+
+    public int hashCode() {
+        return Objects.hash(this.item1, this.item2);
+    }
+}

+ 69 - 67
connector-model/src/main/java/io/connector/model/afarcloud/SimpleObservation.java

@@ -1,67 +1,69 @@
-// Copyright (c) 2020 UWB & LESP.
-// The UWB & LESP license this file to you under the MIT license.
-
-package io.connector.model.afarcloud;
-
-/**
- *
- * @author Lukas Cerny
- * @version 1.0
- * @since 1.0
- */
-public class SimpleObservation {
-
-    private static class Result {
-        private final Double value;
-        private String uom;
-
-        Result(Double value, String uom) {
-            this.value = value;
-            this.uom = uom;
-        }
-
-        Result(Double value) {
-            this.value = value;
-        }
-
-        public Double getValue() {
-            return value;
-        }
-
-        public String getUom() {
-            return uom;
-        }
-    }
-
-    private String observedProperty;
-    private Long resultTime;
-    private Result result;
-
-    public String getObservedProperty() {
-        return observedProperty;
-    }
-
-    public void setObservedProperty(String observedProperty) {
-        this.observedProperty = observedProperty;
-    }
-
-    public Long getResultTime() {
-        return resultTime;
-    }
-
-    public void setResultTime(Long resultTime) {
-        this.resultTime = resultTime;
-    }
-
-    public Result getResult() {
-        return result;
-    }
-
-    public void setResult(Double value, String uom) {
-        this.result = new Result(value, uom);
-    }
-
-    public void setResult(Double value) {
-        this.result = new Result(value);
-    }
-}
+// Copyright (c) 2020 UWB & LESP.
+// The UWB & LESP license this file to you under the MIT license.
+
+package io.connector.model.afarcloud;
+
+/**
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class SimpleObservation {
+
+    private static class Result {
+        private final Double value;
+        private String uom;
+
+        Result(Double value, String uom) {
+            this.value = value;
+            this.uom = uom;
+        }
+
+        Result(Double value) {
+            this.value = value;
+        }
+
+        public Double getValue() {
+            return value;
+        }
+
+        public String getUom() {
+            return uom;
+        }
+    }
+
+    private String observedProperty;
+    private Long resultTime;
+    private Result result;
+
+    public String getObservedProperty() {
+        return observedProperty;
+    }
+
+    public void setObservedProperty(String observedProperty) {
+        this.observedProperty = observedProperty;
+    }
+
+    public Long getResultTime() {
+        return resultTime;
+    }
+
+    public void setResultTime(Long resultTime) {
+        this.resultTime = resultTime;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public void setResult(Double value, String uom) {
+        this.result = new Result(value, uom);
+    }
+
+    public void setResult(Double value) {
+        this.result = new Result(value);
+    }
+
+
+}

+ 275 - 260
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/AFCHttpClient.java

@@ -1,260 +1,275 @@
-// Copyright (c) 2020 UWB & LESP.
-// The UWB & LESP license this file to you under the MIT license.
-
-package io.connector.module.afarcloud;
-
-import io.connector.core.http.HttpClient;
-import io.connector.core.http.HttpRequest;
-import io.connector.core.http.HttpResponse;
-import io.connector.core.http.URLBuilder;
-import io.connector.core.config.HostConfig;
-import io.connector.model.afarcloud.MultiSensor;
-import io.connector.model.afarcloud.MultiSimpleObservation;
-import io.connector.model.afarcloud.ResourceMeasurement;
-import io.vertx.core.json.Json;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static io.connector.core.http.HttpContentType.TEXT_PLAIN;
-import static java.lang.String.format;
-
-/**
- *
- * @author Lukas Cerny
- * @version 1.0
- * @since 1.0
- */
-public class AFCHttpClient {
-
-    private final static Logger logger = LogManager.getLogger(AFCHttpClient.class);
-
-    private static class ResponseOfMeasurement {
-        private ResultOfMeasurement results;
-        public void setResults(ResultOfMeasurement results) {
-            this.results = results;
-        }
-    }
-
-    private static class ResultOfMeasurement {
-        private List<ResourceMeasurement> resources;
-        void setResources(List<ResourceMeasurement> resources) {
-            this.resources = resources;
-        }
-    }
-
-    private static class ResultOfMultiSensor {
-        private List<MultiSensor> sensors;
-        void setSensors(List<MultiSensor> sensors) {
-            this.sensors = sensors;
-        }
-    }
-
-    private final AFCConfig config;
-
-    private final HttpClient httpClient;
-
-    public AFCHttpClient(AFCConfig config, HttpClient httpClient) {
-        this.config = config;
-        this.httpClient = httpClient;
-    }
-
-    public void uploadAggregatedMeasurements(List<MultiSimpleObservation> observations) {
-
-        if (observations == null || observations.isEmpty()) {
-            logger.warn("Nothing to upload. Received observations of multi-sensor are empty."); return;
-        }
-
-        HostConfig host = new HostConfig(config.getTelemetryDomain(), "telemetry");
-
-        List<MultiSimpleObservation> failedObservations = new ArrayList<>();
-
-        for (MultiSimpleObservation multiSensor : observations) {
-
-            HttpRequest request = HttpRequest.newBuilder().POST()
-                    .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
-                            .addParam("test", "")
-                            .build())
-                    .body(Json.encode(multiSensor))
-                    .build();
-            logger.info("Creating a http request to {}.", request);
-
-            HttpResponse response = httpClient.send(request);
-            logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
-
-            if (response.isError()) {
-                failedObservations.add(multiSensor);
-            }
-        }
-
-        logger.info("Uploaded {}/{} observations.", observations.size()-failedObservations.size(), observations.size());
-
-        if (!failedObservations.isEmpty()) {
-            StringBuilder resourceIdBuilder = new StringBuilder("[");
-            for (int i = 0; i < failedObservations.size() - 1; i++) {
-                resourceIdBuilder.append(failedObservations.get(i).getResourceId());
-            }
-            resourceIdBuilder.append(failedObservations.get(failedObservations.size()-1)).append("]");
-            throw logger.throwing(new RuntimeException(String.format(
-                    "Observations for following resourceIds can not be send: %s.", resourceIdBuilder.toString()
-            )));
-        }
-    }
-
-    public List<ResourceMeasurement> getObservationsBySensor(Filter filter) {
-        HostConfig host = new HostConfig(config.getRetrievalDomain(), "getObservationsBySensor/historic");
-        logger.debug("Getting observations from {}.", host.getDomain());
-
-        URLBuilder urlBuilder = URLBuilder.newBuilder(host.getDomain(), host.getPath());
-
-        if (filter.startTime == null) {
-            throw new RuntimeException("Parameter 'startTime' is required.");
-        }
-        urlBuilder.addParam("start_time", filter.startTime());
-
-        if (filter.endTime != null) {
-            urlBuilder.addParam("end_time", filter.endTime());
-        }
-        if (filter.entityNames != null) {
-            urlBuilder.addParam("entityNames", filter.entityNames());
-        }
-        if (filter.measurements != null) {
-            urlBuilder.addParam("measurements", filter.measurements());
-        }
-        if (filter.order != null) {
-            urlBuilder.addParam("order", filter.order());
-        }
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .contentType(TEXT_PLAIN).url(urlBuilder.build()).GET().build();
-        logger.info("Creating a http request to {}.", request);
-
-        HttpResponse response = httpClient.send(request);
-        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
-
-        if (response.isError()) {
-            throw logger.throwing(new RuntimeException(String.format(
-                    "Can not get data from the server %s. Error %d %s",
-                    host.getDomain(), response.getStatus(), response.getBody())
-            ));
-        }
-
-        logger.debug("Parsing body of the response to the class {}.", ResponseOfMeasurement.class);
-        ResponseOfMeasurement responseModel = Json.decodeValue(response.getBody(), ResponseOfMeasurement.class);
-        if (responseModel == null) {
-            throw logger.throwing(new RuntimeException(String.format(
-                    "Received JSON data can not be parsed. Data: %s", response.getBody()
-            )));
-        }
-
-        if (responseModel.results == null || responseModel.results.resources == null) {
-            return Collections.emptyList();
-        }
-        return responseModel.results.resources;
-    }
-
-    public List<ResourceMeasurement> getLatestObservationsBySensor(Filter filter) {
-        HostConfig host = new HostConfig(config.getRetrievalDomain(), "getObservationsBySensor/latest");
-        logger.debug("Getting observations from {}.", host.getDomain());
-
-        URLBuilder urlBuilder = URLBuilder.newBuilder(host.getDomain(), host.getPath());
-
-        if (filter.limit > 0) {
-            logger.debug("Adding a parameter 'limit' to the request.");
-            urlBuilder.addParam("limit", filter.limit());
-        }
-        if (filter.entityNames != null) {
-            logger.debug("Adding a parameter 'entityNames' to the request.");
-            urlBuilder.addParam("entityNames", filter.entityNames());
-        }
-        if (filter.measurements != null) {
-            logger.debug("Adding a parameter 'measurements' to the request.");
-            urlBuilder.addParam("measurements", filter.measurements());
-        }
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .contentType(TEXT_PLAIN).url(urlBuilder.build()).GET().build();
-        logger.info("Creating a http request to {}.", request);
-
-        HttpResponse response = httpClient.send(request);
-        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
-
-        if (response.isError()) {
-            logger.error("Can not get data from the server {}. Error {} {}",
-                    host.getDomain(), response.getStatus(), response.getBody());
-            throw new RuntimeException(response.getBody());
-        }
-
-        logger.debug("Parsing body of the response to the class {}.", ResponseOfMeasurement.class);
-        ResponseOfMeasurement responseModel = Json.decodeValue(response.getBody(), ResponseOfMeasurement.class);
-        if (responseModel == null || responseModel.results == null || responseModel.results.resources == null) {
-            return Collections.emptyList();
-        }
-        return responseModel.results.resources;
-    }
-
-    public List<MultiSensor> getAllSensors() {
-        HostConfig host = new HostConfig(config.getInfoDomain(), "registry/getAllSensors");
-        logger.debug("Getting observations from {}.", host.getDomain());
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .contentType(TEXT_PLAIN)
-                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath()).build())
-                .GET().build();
-        logger.info("Creating a http request to {}.", request);
-
-        HttpResponse response = httpClient.send(request);
-        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
-
-        if (response.isError()) {
-            logger.error("Can not get data from the server {}. Error {} {}",
-                    host.getDomain(), response.getStatus(), response.getBody());
-            throw new RuntimeException(response.getBody());
-        }
-
-        logger.debug("Parsing body of the response to the class {}.", ResultOfMultiSensor.class);
-        ResultOfMultiSensor resultModel = Json.decodeValue(format("{\"sensors\":%s}", response.getBody()), ResultOfMultiSensor.class);
-        if (resultModel == null || resultModel.sensors == null) {
-            return Collections.emptyList();
-        }
-        return resultModel.sensors;
-    }
-
-    public MultiSensor getSensorByResourceId(String resourceId) {
-        HostConfig host = new HostConfig(config.getInfoDomain(), format("registry/getSensor/%s", resourceId));
-        logger.debug("Getting observations from {}.", host.getDomain());
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .contentType(TEXT_PLAIN)
-                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath()).build())
-                .GET().build();
-        logger.info("Creating a http request to {}.", request);
-
-        HttpResponse response = httpClient.send(request);
-        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
-
-        if (response.isError()) {
-            logger.error("Can not get data from the server {}. Error {} {}",
-                    host.getDomain(), response.getStatus(), response.getBody());
-            throw new RuntimeException(response.getBody());
-        }
-
-        logger.debug("Parsing body of the response to the class {}.", MultiSensor.class);
-        return Json.decodeValue(response.getBody(), MultiSensor.class);
-    }
-
-    public MultiSensor getSensorByResourceUrn(String resourceUrn) {
-        List<MultiSensor> multiSensors = getAllSensors();
-        for (MultiSensor multiSensor : multiSensors) {
-            if (multiSensor.getResourceUrn().equals(resourceUrn)) {
-                return multiSensor;
-            }
-        }
-        throw new IllegalArgumentException(
-                "Can not find sensor by resourceUrn '" + resourceUrn + "'."
-        );
-    }
-}
+// Copyright (c) 2020 UWB & LESP.
+// The UWB & LESP license this file to you under the MIT license.
+
+package io.connector.module.afarcloud;
+
+import io.connector.core.http.HttpClient;
+import io.connector.core.http.HttpRequest;
+import io.connector.core.http.HttpResponse;
+import io.connector.core.http.URLBuilder;
+import io.connector.core.config.HostConfig;
+import io.connector.core.util.Tuple;
+import io.connector.model.afarcloud.MultiSensor;
+import io.connector.model.afarcloud.MultiSimpleObservation;
+import io.connector.model.afarcloud.ResourceMeasurement;
+import io.connector.model.afarcloud.SimpleObservation;
+import io.vertx.core.json.Json;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import static io.connector.core.http.HttpContentType.TEXT_PLAIN;
+import static java.lang.String.format;
+
+/**
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AFCHttpClient {
+
+    private final static Logger logger = LogManager.getLogger(AFCHttpClient.class);
+
+    private static class ResponseOfMeasurement {
+        private ResultOfMeasurement results;
+        public void setResults(ResultOfMeasurement results) {
+            this.results = results;
+        }
+    }
+
+    private static class ResultOfMeasurement {
+        private List<ResourceMeasurement> resources;
+        void setResources(List<ResourceMeasurement> resources) {
+            this.resources = resources;
+        }
+    }
+
+    private static class ResultOfMultiSensor {
+        private List<MultiSensor> sensors;
+        void setSensors(List<MultiSensor> sensors) {
+            this.sensors = sensors;
+        }
+    }
+
+    private final AFCConfig config;
+
+    private final HttpClient httpClient;
+
+    public AFCHttpClient(AFCConfig config, HttpClient httpClient) {
+        this.config = config;
+        this.httpClient = httpClient;
+    }
+
+    public void uploadAggregatedMeasurements(List<MultiSimpleObservation> observations) {
+
+        if (observations == null || observations.isEmpty()) {
+            logger.warn("Nothing to upload. Received observations of multi-sensor are empty."); return;
+        }
+
+        HostConfig host = new HostConfig(config.getTelemetryDomain(), "telemetry");
+
+        List<Tuple<MultiSimpleObservation, String>> failedObservations = new ArrayList<>();
+
+        for (MultiSimpleObservation multiSensor : observations) {
+
+            HttpRequest request = HttpRequest.newBuilder().POST()
+                    .url(URLBuilder.newBuilder(host.getDomain(), host.getPath())
+                        //    .addParam("test", "")
+                            .build())
+                    .body(Json.encode(multiSensor))
+                    .build();
+            logger.info("Creating a http request to {}.", request);
+
+            HttpResponse response = httpClient.send(request);
+            logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+            if (response.isError()) {
+                failedObservations.add(Tuple.of(multiSensor, response.getBody()));
+            }
+        }
+
+        logger.info("Uploaded {}/{} observations.", observations.size()-failedObservations.size(), observations.size());
+
+        if (!failedObservations.isEmpty()) {
+            final Function<Tuple<MultiSimpleObservation, String>, String> formatError = t -> {
+                List<SimpleObservation> sObs = t.getItem1().getObservations();
+                StringBuilder propertiesBuilder = new StringBuilder("[");
+                for (int obI = 0; obI < sObs.size() - 1; obI++) {
+                    propertiesBuilder.append(sObs.get(obI).getObservedProperty()).append(",");
+                }
+                propertiesBuilder.append(sObs.get(sObs.size() - 1).getObservedProperty()).append("]");
+                return "resourceId: " + t.getItem1().getResourceId() +
+                        " with observedProperty: " + propertiesBuilder +
+                        " appeared an error: " + t.getItem2();
+            };
+
+            StringBuilder resourceIdBuilder = new StringBuilder("[");
+            for (int i = 0; i < failedObservations.size() - 1; i++) {
+                resourceIdBuilder.append(formatError.apply(failedObservations.get(i))).append(", ");
+            }
+            resourceIdBuilder.append(formatError.apply(failedObservations.get(failedObservations.size()-1))).append("]");
+            throw logger.throwing(new RuntimeException(String.format(
+                    "Observations for following resourceIds can not be send: %s.", resourceIdBuilder
+            )));
+        }
+    }
+
+    public List<ResourceMeasurement> getObservationsBySensor(Filter filter) {
+        HostConfig host = new HostConfig(config.getRetrievalDomain(), "getObservationsBySensor/historic");
+        logger.debug("Getting observations from {}.", host.getDomain());
+
+        URLBuilder urlBuilder = URLBuilder.newBuilder(host.getDomain(), host.getPath());
+
+        if (filter.startTime == null) {
+            throw new RuntimeException("Parameter 'startTime' is required.");
+        }
+        urlBuilder.addParam("start_time", filter.startTime());
+
+        if (filter.endTime != null) {
+            urlBuilder.addParam("end_time", filter.endTime());
+        }
+        if (filter.entityNames != null) {
+            urlBuilder.addParam("entityNames", filter.entityNames());
+        }
+        if (filter.measurements != null) {
+            urlBuilder.addParam("measurements", filter.measurements());
+        }
+        if (filter.order != null) {
+            urlBuilder.addParam("order", filter.order());
+        }
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .contentType(TEXT_PLAIN).url(urlBuilder.build()).GET().build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isError()) {
+            throw logger.throwing(new RuntimeException(String.format(
+                    "Can not get data from the server %s. Error %d %s",
+                    host.getDomain(), response.getStatus(), response.getBody())
+            ));
+        }
+
+        logger.debug("Parsing body of the response to the class {}.", ResponseOfMeasurement.class);
+        ResponseOfMeasurement responseModel = Json.decodeValue(response.getBody(), ResponseOfMeasurement.class);
+        if (responseModel == null) {
+            throw logger.throwing(new RuntimeException(String.format(
+                    "Received JSON data can not be parsed. Data: %s", response.getBody()
+            )));
+        }
+
+        if (responseModel.results == null || responseModel.results.resources == null) {
+            return Collections.emptyList();
+        }
+        return responseModel.results.resources;
+    }
+
+    public List<ResourceMeasurement> getLatestObservationsBySensor(Filter filter) {
+        HostConfig host = new HostConfig(config.getRetrievalDomain(), "getObservationsBySensor/latest");
+        logger.debug("Getting observations from {}.", host.getDomain());
+
+        URLBuilder urlBuilder = URLBuilder.newBuilder(host.getDomain(), host.getPath());
+
+        if (filter.limit > 0) {
+            logger.debug("Adding a parameter 'limit' to the request.");
+            urlBuilder.addParam("limit", filter.limit());
+        }
+        if (filter.entityNames != null) {
+            logger.debug("Adding a parameter 'entityNames' to the request.");
+            urlBuilder.addParam("entityNames", filter.entityNames());
+        }
+        if (filter.measurements != null) {
+            logger.debug("Adding a parameter 'measurements' to the request.");
+            urlBuilder.addParam("measurements", filter.measurements());
+        }
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .contentType(TEXT_PLAIN).url(urlBuilder.build()).GET().build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isError()) {
+            logger.error("Can not get data from the server {}. Error {} {}",
+                    host.getDomain(), response.getStatus(), response.getBody());
+            throw new RuntimeException(response.getBody());
+        }
+
+        logger.debug("Parsing body of the response to the class {}.", ResponseOfMeasurement.class);
+        ResponseOfMeasurement responseModel = Json.decodeValue(response.getBody(), ResponseOfMeasurement.class);
+        if (responseModel == null || responseModel.results == null || responseModel.results.resources == null) {
+            return Collections.emptyList();
+        }
+        return responseModel.results.resources;
+    }
+
+    public List<MultiSensor> getAllSensors() {
+        HostConfig host = new HostConfig(config.getInfoDomain(), "registry/getAllSensors");
+        logger.debug("Getting observations from {}.", host.getDomain());
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .contentType(TEXT_PLAIN)
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath()).build())
+                .GET().build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isError()) {
+            logger.error("Can not get data from the server {}. Error {} {}",
+                    host.getDomain(), response.getStatus(), response.getBody());
+            throw new RuntimeException(response.getBody());
+        }
+
+        logger.debug("Parsing body of the response to the class {}.", ResultOfMultiSensor.class);
+        ResultOfMultiSensor resultModel = Json.decodeValue(format("{\"sensors\":%s}", response.getBody()), ResultOfMultiSensor.class);
+        if (resultModel == null || resultModel.sensors == null) {
+            return Collections.emptyList();
+        }
+        return resultModel.sensors;
+    }
+
+    public MultiSensor getSensorByResourceId(String resourceId) {
+        HostConfig host = new HostConfig(config.getInfoDomain(), format("registry/getSensor/%s", resourceId));
+        logger.debug("Getting observations from {}.", host.getDomain());
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .contentType(TEXT_PLAIN)
+                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath()).build())
+                .GET().build();
+        logger.info("Creating a http request to {}.", request);
+
+        HttpResponse response = httpClient.send(request);
+        logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), host.getDomain());
+
+        if (response.isError()) {
+            logger.error("Can not get data from the server {}. Error {} {}",
+                    host.getDomain(), response.getStatus(), response.getBody());
+            throw new RuntimeException(response.getBody());
+        }
+
+        logger.debug("Parsing body of the response to the class {}.", MultiSensor.class);
+        return Json.decodeValue(response.getBody(), MultiSensor.class);
+    }
+
+    public MultiSensor getSensorByResourceUrn(String resourceUrn) {
+        List<MultiSensor> multiSensors = getAllSensors();
+        for (MultiSensor multiSensor : multiSensors) {
+            if (multiSensor.getResourceUrn().equals(resourceUrn)) {
+                return multiSensor;
+            }
+        }
+        throw new IllegalArgumentException(
+                "Can not find sensor by resourceUrn '" + resourceUrn + "'."
+        );
+    }
+}

+ 1029 - 1021
connector-module-afarcloud/src/main/java/io/connector/module/afarcloud/gateway/OGCSensorThingsGateway.java

@@ -1,1021 +1,1029 @@
-// Copyright (c) 2020 UWB & LESP.
-// The UWB & LESP license this file to you under the MIT license.
-
-package io.connector.module.afarcloud.gateway;
-
-import io.connector.core.AbstractGateway;
-import io.connector.core.http.RequestUriComponent;
-import io.connector.model.afarcloud.*;
-import io.connector.model.sensorthings.*;
-import io.connector.model.sensorthings.Location;
-import io.connector.model.sensorthings.Observation;
-import io.connector.module.afarcloud.AFCHttpClient;
-import io.connector.module.afarcloud.Filter;
-import io.vertx.core.json.Json;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
-import io.vertx.ext.web.handler.BodyHandler;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.time.Instant;
-import java.util.*;
-import java.util.function.Supplier;
-
-import static io.connector.core.http.HttpContentType.APPLICATION_JSON;
-import static io.connector.core.http.HttpHeader.CONTENT_TYPE;
-import static io.connector.core.AddressPath.Creator.create;
-import static io.vertx.core.json.Json.encode;
-import static java.lang.Math.sqrt;
-import static java.lang.StrictMath.pow;
-import static java.lang.String.format;
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-import static java.util.Optional.ofNullable;
-
-/**
- *
- * @author Lukas Cerny
- * @version 1.0
- * @since 1.0
- */
-public class OGCSensorThingsGateway extends AbstractGateway {
-
-    private final static Logger logger = LogManager.getLogger(OGCSensorThingsGateway.class);
-
-    /** Attribute of a client that accesses to the AFarCloud system.  */
-    private final AFCHttpClient client;
-
-    /**
-     * Public constructor of the class that initializes final attributes.
-     * @param id - identifier of the gateway
-     * @param client - client allows access to the AFarCloud system
-     */
-    public OGCSensorThingsGateway(String id, AFCHttpClient client) {
-        super(id);
-        this.client = client;
-    }
-
-    @Override
-    protected void run() {
-
-        router().post().handler(BodyHandler.create()).handler(ctx -> {
-            String contentType = ctx.request().getHeader(CONTENT_TYPE);
-            if (!contentType.equals(APPLICATION_JSON)) {
-                ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).setStatusCode(415)
-                        .end(new JsonObject().put("message", String.format(
-                                "Unsupported content type. Use one of these [%s].", APPLICATION_JSON
-                        )).encode()
-                );
-            }
-            ctx.next();
-        });
-
-        router().get(create("Things")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            List<MultiSensor> afcMultiSensors = client.getAllSensors();
-            List<Thing> ogcThings = Converter.convertToThing(afcMultiSensors, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(encode(ogcThings));
-        });
-
-        router().get(create("Things(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String resourceUrn = ctx.pathParam("id"); // resourceUrn
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            Thing ogcThing = Converter.convertToThing(afcMultiSensor, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcThing.encode());
-        });
-
-        router().get(create("Datastreams(:id)/Thing")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            String[] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0];
-            ctx.reroute(format("%s/Things(%s)", uriComponent.getGatewayPath(), resourceUrn));
-        });
-
-        router().get(create("HistoricalLocations(:id)/Thing")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
-            String [] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0];
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            Thing ogcThing = Converter.convertToThing(afcMultiSensor, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcThing.encode());
-        });
-
-        router().get(create("Datastreams(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            String[] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0], observedProperty = idCmp[1];
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            List<ResourceMeasurement> afcMeasurements = client.getLatestObservationsBySensor(new Filter()
-                    .limit(1).entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
-            );
-            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Datastream with @iot.id \"" + id + "\".");
-
-            Optional<MultiSensor.SensorSchema> afcSensorOpt = afcMultiSensor.getObservations().stream().filter(s -> s.getObservedProperty().equals(observedProperty)).findFirst();
-            MultiSensor.SensorSchema afcSensor = afcSensorOpt.orElseThrow(exception);
-
-            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
-            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
-
-            Optional<SensorTelemetry> afcTelemetryOpt = ofNullable(afcMeasurement.getMeasurements().size() == 1 ? afcMeasurement.getMeasurements().get(0) : null);
-            SensorTelemetry afcTelemetry = afcTelemetryOpt.orElseThrow(exception);
-
-            Datastream datastream = Converter.convertToDataStream(afcMultiSensor, afcSensor, afcTelemetry, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(datastream.encode());
-        });
-
-        router().get(create("Things(:id)/Datastreams")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String resourceUrn = ctx.pathParam("id"); // resourceUrn
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            List<ResourceMeasurement> afcMeasurements = client.getLatestObservationsBySensor(new Filter()
-                    .limit(1)
-                    .entityNames(afcMultiSensor.getResourceId())
-            );
-            ResourceMeasurement afcMeasurement = afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null;
-            List<Datastream> ogcDataStream = Converter.convertToDataStream(afcMultiSensor, afcMeasurement, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(encode(ogcDataStream));
-        });
-
-        router().get(create("Sensors(:id)/Datastreams")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String resourceUrn = ctx.pathParam("id"); // resourceUrn
-            ctx.reroute(format("%s/Things(%s)/Datastreams", uriComponent.getGatewayPath(), resourceUrn));
-        });
-
-        router().get(create("ObservedProperties(:id)/Datastream")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            ctx.reroute(format("%s/Datastreams(%s)", uriComponent.getGatewayPath(), id));
-        });
-
-        router().get(create("Observations(:id)/Datastream")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resource + measurement + time
-            String[] idCmp = Converter.disassemblyId(id);
-            String resource = idCmp[0], measurement = idCmp[1];
-            MultiSensor afcMultiSensor = client.getSensorByResourceId(resource);
-            String resourceUrn = afcMultiSensor.getResourceUrn();
-            ctx.reroute(format("%s/Datastreams(%s)", uriComponent.getGatewayPath(), Converter.assemblyId(resourceUrn, measurement)));
-        });
-
-        router().get(create("Sensors(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String resourceUrn = ctx.pathParam("id"); // resourceUrn
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            Sensor ogcSensor = Converter.convertToSensor(afcMultiSensor, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcSensor.encode());
-        });
-
-        router().get(create("Datastreams(:id)/Sensor")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            String[] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0];
-            ctx.reroute(format("%s/Sensors(%s)", uriComponent.getGatewayPath(), resourceUrn));
-        });
-
-        router().get(create("ObservedProperties(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            String[] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0], observedProperty = idCmp[1];
-            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Datastream with @iot.id \"" + id + "\".");
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            Optional<MultiSensor.SensorSchema> afcSensorOpt = afcMultiSensor.getObservations().stream().filter(s -> s.getObservedProperty().equals(observedProperty)).findFirst();
-            MultiSensor.SensorSchema afcSensor = afcSensorOpt.orElseThrow(exception);
-            ObservedProperty property = Converter.convertToObservedProperty(afcMultiSensor, afcSensor, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(property.encode());
-        });
-
-        router().get(create("Datastreams(:id)/ObservedProperty")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            ctx.reroute(format("%s/ObservedProperties(%s)", uriComponent.getGatewayPath(), id));
-        });
-
-        router().get(create("Observations(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resource + measurement + time
-            String[] idCmp = Converter.disassemblyId(id);
-            String resource = idCmp[0], measurement = idCmp[1], time = idCmp[2];
-
-            Instant timestamp = Instant.parse(time);
-
-            List<ResourceMeasurement> measurements = client.getObservationsBySensor(new Filter()
-                    .limit(1).order("ASC").entityNames(resource).measurements(measurement)
-                    .startTime(timestamp).endTime(timestamp.plusSeconds(60))
-            );
-            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException(format(
-                    "Can not find Observation with @iot.id '%s'.", id));
-
-            Optional<ResourceMeasurement> measurementOpt = ofNullable(measurements.size() == 1 ? measurements.get(0) : null);
-            ResourceMeasurement afcMeasurement = measurementOpt.orElseThrow(exception);
-
-            Optional<SensorTelemetry> telemetryOpt = ofNullable(afcMeasurement.getMeasurements().size() == 1 ? afcMeasurement.getMeasurements().get(0) : null);
-            SensorTelemetry afcTelemetry = telemetryOpt.orElseThrow(exception);
-
-            Optional<io.connector.model.afarcloud.Observation> observationOpt = ofNullable(afcTelemetry.getObservations().size() > 0 ? afcTelemetry.getObservations().get(0) : null);
-            io.connector.model.afarcloud.Observation afcObservation = observationOpt.orElseThrow(exception);
-
-            Observation ogcObservation = Converter.convertToObservation(afcMeasurement, afcTelemetry, afcObservation, uriComponent);
-            ctx.response().end(ogcObservation.encode());
-        });
-
-        router().get(create("Datastreams(:id)/Observations")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
-            String filterParam = ctx.request().getParam(Params.FILTER);
-
-            io.connector.model.sensorthings.Filter filter = null;
-            try {
-                filter = io.connector.model.sensorthings.Filter.parse(filterParam);
-            } catch (RuntimeException e) {
-                ctx.response().setStatusCode(501).end(new JsonObject()
-                        .put("message", e.getMessage()).encode()); return;
-            }
-
-            String [] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0], observedProperty = idCmp[1];
-
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-
-            List<ResourceMeasurement> measurements;
-            if (filter.isExists()) {
-                Filter afcFilter;
-                try {
-                    afcFilter = Converter.convertFilter(filter);
-                } catch (RuntimeException e) {
-                    ctx.response().setStatusCode(501).end(new JsonObject()
-                            .put("message", e.getMessage()).encode()); return;
-                }
-                afcFilter.entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty);
-                measurements = client.getObservationsBySensor(afcFilter);
-            } else {
-                measurements = client.getLatestObservationsBySensor(new Filter()
-                        .limit(1).entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
-                );
-            }
-
-            if (measurements.isEmpty()) {
-                ctx.response().end(new JsonArray().encode());
-            } else {
-                final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Datastream with @iot.id \"" + id + "\".");
-
-                Optional<ResourceMeasurement> measurementOpt = ofNullable(measurements.size() == 1 ? measurements.get(0) : null);
-                ResourceMeasurement afcMeasurement = measurementOpt.orElseThrow(exception);
-
-                Optional<SensorTelemetry> telemetryOpt = ofNullable(afcMeasurement.getMeasurements().size() == 1 ? afcMeasurement.getMeasurements().get(0) : null);
-                SensorTelemetry afcTelemetry = telemetryOpt.orElseThrow(exception);
-
-                List<Observation> ogcObservations = Converter.convertToObservations(afcMeasurement, afcTelemetry, uriComponent);
-                ctx.response().end(encode(ogcObservations));
-            }
-        });
-
-        router().get(create("FeaturesOfInterest(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id");
-            FeatureOfInterest featureOfInterest = new FeatureOfInterest();
-            featureOfInterest.setId(id);
-            featureOfInterest.setSelfLink("https://storage07-afarcloud.qa.pdmfc.com/storage/rest/registry/getAllObservationTypes");
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(featureOfInterest.encode());
-        });
-
-        router().get(create("Locations(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
-            String [] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0], observedProperty = idCmp[1], time = idCmp[2];
-
-            Instant startTime = Instant.parse(time);
-            Instant endTime = startTime.plusSeconds(60);
-
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            List<ResourceMeasurement> afcMeasurements = client.getObservationsBySensor(new Filter()
-                    .startTime(startTime).endTime(endTime)
-                    .entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
-            );
-            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Location with @iot.id \"" + resourceUrn + "\".");
-
-            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
-            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
-
-            AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
-            Optional<AFCAggrLocation> afcLastLocationOpt = ofNullable(afcLocations.getLast());
-            AFCAggrLocation afcLastLocation = afcLastLocationOpt.orElseThrow(exception);
-
-            Location ogcLocation = Converter.convertToLocation(afcMultiSensor, afcLastLocation, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcLocation.encode());
-        });
-
-        router().get(create("Things(:id)/Locations")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String resourceUrn = ctx.pathParam("id");  // resourceUrn
-
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            List<ResourceMeasurement> afcMeasurements = client.getLatestObservationsBySensor(new Filter()
-                    .limit(1).entityNames(afcMultiSensor.getResourceId())
-            );
-            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Thing with @iot.id \"" + resourceUrn + "\".");
-
-            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
-            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
-
-            AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
-            Optional<AFCAggrLocation> afcLastLocationOpt = ofNullable(afcLocations.getFirst());
-            AFCAggrLocation afcLastLocation = afcLastLocationOpt.orElseThrow(exception);
-
-            Location ogcLocation = Converter.convertToLocation(afcMultiSensor, afcLastLocation, uriComponent);
-            List<Location> ogcLocations = singletonList(ogcLocation);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(encode(ogcLocations));
-        });
-
-        router().get(create("HistoricalLocations(:id)/Locations")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
-            ctx.reroute(format("%s/Locations(%s)", uriComponent.getGatewayPath(), id));
-        });
-
-        router().get(create("HistoricalLocations(:id)")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
-
-            String [] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0], observedProperty = idCmp[1], time = idCmp[2];
-
-            Instant startTime = Instant.parse(time);
-            Instant endTime = startTime.plusSeconds(60);
-
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            List<ResourceMeasurement> afcMeasurements = client.getObservationsBySensor(new Filter()
-                    .startTime(startTime).endTime(endTime)
-                    .entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
-            );
-            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Location with @iot.id \"" + resourceUrn + "\".");
-
-            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
-            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
-
-            AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
-            Optional<AFCAggrLocation> afcLastLocationOpt = ofNullable(afcLocations.getLast());
-            AFCAggrLocation afcLocation = afcLastLocationOpt.orElseThrow(exception);
-
-            HistoricalLocation ogcLocation = Converter.convertToHistoricalLocation(afcMultiSensor, afcLocation, uriComponent);
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcLocation.encode());
-        });
-
-        router().get(create("Things(:id)/HistoricalLocations")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String resourceUrn = ctx.pathParam("id"); // resourceUrn
-            String filterParam = ctx.request().getParam(Params.FILTER);
-
-            io.connector.model.sensorthings.Filter filter = null;
-            try {
-                filter = io.connector.model.sensorthings.Filter.parse(filterParam);
-            } catch (RuntimeException e) {
-                ctx.response().setStatusCode(501).end(new JsonObject()
-                        .put("message", e.getMessage()).encode()); return;
-            }
-
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-            List<ResourceMeasurement> afcMeasurements;
-            if (filter.isExists()) {
-                Filter afcFilter;
-                try {
-                    afcFilter = Converter.convertFilter(filter);
-                } catch (RuntimeException e) {
-                    ctx.response().setStatusCode(501).end(new JsonObject()
-                            .put("message", e.getMessage()).encode()); return;
-                }
-                afcFilter.entityNames(afcMultiSensor.getResourceId());
-                afcMeasurements = client.getObservationsBySensor(afcFilter);
-            } else {
-                afcMeasurements = client.getLatestObservationsBySensor(new Filter()
-                        .limit(1).entityNames(afcMultiSensor.getResourceId())
-                );
-            }
-
-            if (afcMeasurements.isEmpty()) {
-                ctx.response().end(new JsonArray().encode());
-            } else {
-                final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find HistoricalLocations of the Thing with @iot.id \"" + resourceUrn + "\".");
-
-                Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
-                ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
-
-                AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
-
-                List<HistoricalLocation> locations = Converter.convertToHistoricalLocation(afcMultiSensor, afcLocations.getList(), uriComponent);
-                ctx.response().end(encode(locations));
-            }
-        });
-
-        router().get(create("Locations(:id)/HistoricalLocations")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
-            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
-            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
-            String filterParam = ctx.request().getParam(Params.FILTER);
-
-            io.connector.model.sensorthings.Filter filter = null;
-            try {
-                filter = io.connector.model.sensorthings.Filter.parse(filterParam);
-            } catch (RuntimeException e) {
-                ctx.response().setStatusCode(501).end(new JsonObject()
-                        .put("message", e.getMessage()).encode()); return;
-            }
-
-            String [] idCmp = Converter.disassemblyId(id);
-            String resourceUrn = idCmp[0], observedProperty = idCmp[1], time = idCmp[2];
-
-            final Supplier<IllegalArgumentException> exception =
-                    () -> new IllegalArgumentException("Can not find HistoricalLocations of the Thing with @iot.id \"" + resourceUrn + "\".");
-
-            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
-
-            Filter afcFilter;
-            if (filter.isExists()) {
-                try {
-                    afcFilter = Converter.convertFilter(filter);
-                } catch (RuntimeException e) {
-                    ctx.response().setStatusCode(501).end(new JsonObject()
-                            .put("message", e.getMessage()).encode()); return;
-                }
-                afcFilter.entityNames(afcMultiSensor.getResourceId());
-            } else {
-                Instant startTime = Instant.parse(time);
-                Instant endTime = startTime.plusSeconds(60*60); // 1h
-
-                afcFilter = new Filter().startTime(startTime).endTime(endTime);
-            }
-
-            List<ResourceMeasurement> afcMeasurements = client.getObservationsBySensor(afcFilter
-                    .entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
-            );
-
-            if (afcMeasurements.isEmpty()) {
-                ctx.response().end(new JsonArray().encode());
-            } else {
-
-                Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
-                ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
-
-                AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
-
-                List<HistoricalLocation> locations = Converter.convertToHistoricalLocation(afcMultiSensor, afcLocations.getList(), uriComponent);
-                ctx.response().end(encode(locations));
-            }
-        });
-
-        router().post(create("Datastream")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
-
-            // TODO catch exception and create a wrapper with http/error code
-            ObservationInsert ogcObservation = ObservationInsert.parse(ctx.getBodyAsJson());
-            MultiSimpleObservation afcObservation = Converter.convertToMultiSimpleObservation(ogcObservation);
-            client.uploadAggregatedMeasurements(singletonList(afcObservation));
-            // TODO response http 201 (created) and link to the Datastream
-            ctx.response().end();
-        });
-
-        router().post(create("Things")).handler(ctx -> {
-            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
-            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
-
-            ThingInsert ogcThing = ThingInsert.parse(ctx.getBodyAsJson());
-            MultiSensor afcMultiSensor = Converter.convertToMultiSensor(ogcThing);
-
-            ctx.response().end(Json.encode(afcMultiSensor));
-        });
-    }
-
-    private static class AFCAggrLocation implements Comparable<AFCAggrLocation> {
-        private final String measurement;
-        private final io.connector.model.afarcloud.Observation observation;
-
-        public AFCAggrLocation(String measurement, io.connector.model.afarcloud.Observation observation) {
-            this.measurement = measurement;
-            this.observation = observation;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            AFCAggrLocation that = (AFCAggrLocation) o;
-            return this.compareTo(that) == 0;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(measurement, observation);
-        }
-
-        @Override
-        public int compareTo(AFCAggrLocation o) {
-            io.connector.model.afarcloud.Observation o1 = this.observation;
-            io.connector.model.afarcloud.Observation o2 = o.observation;
-
-            Instant o1Time = Instant.parse(o1.getTime());
-            Instant o2Time = Instant.parse(o2.getTime());
-
-            if (o1Time.equals(o2Time)) {
-                Double o1Dist = sqrt(pow(o1.getLongitude(), 2) + pow(o1.getLatitude(), 2) + pow(o1.getAltitude(), 2));
-                Double o2Dist = sqrt(pow(o2.getLongitude(), 2) + pow(o2.getLatitude(), 2) + pow(o2.getAltitude(), 2));
-                return o1Dist.compareTo(o2Dist);
-            } else {
-                return o1Time.compareTo(o2Time);
-            }
-        }
-    }
-
-    private static class AFCLocationList {
-        private final List<AFCAggrLocation> list;
-
-        AFCLocationList() {
-            this.list = new ArrayList<>();
-        }
-
-        public void sort() {
-            Collections.sort(list);
-        }
-
-        public void add(AFCAggrLocation location) {
-            list.add(location);
-        }
-
-        public AFCAggrLocation getFirst() {
-            return list.isEmpty() ? null : list.get(0);
-        }
-
-        public AFCAggrLocation getLast() {
-            return list.isEmpty() ? null : list.get(list.size()-1);
-        }
-
-        public AFCAggrLocation get(int index) {
-            return list.get(index);
-        }
-
-        public List<AFCAggrLocation> getList() {
-            return list;
-        }
-    }
-
-    private static class AFCLocationUtils {
-
-        static AFCLocationList sort(ResourceMeasurement afcMeasurement) {
-            Objects.requireNonNull(afcMeasurement);
-
-            AFCLocationList aggrLocations = new AFCLocationList();
-            for (SensorTelemetry measurement : afcMeasurement.getMeasurements()) {
-                for (io.connector.model.afarcloud.Observation observation : measurement.getObservations()) {
-                    aggrLocations.add(new AFCAggrLocation(measurement.getMeasurement(), observation));
-                }
-            }
-            aggrLocations.sort();
-
-            List<AFCAggrLocation> list = aggrLocations.list;
-            if (list.size() > 1) {
-                Iterator<AFCAggrLocation> iterator = list.iterator();
-                AFCAggrLocation previous = iterator.next();
-                while (iterator.hasNext()) {
-                    AFCAggrLocation current = iterator.next();
-                    if (previous.equals(current)) {
-                        iterator.remove(); continue;
-                    }
-                    previous = current;
-                }
-            }
-
-            return aggrLocations;
-        }
-    }
-
-    private static class Converter {
-
-        private final static Logger logger = LogManager.getLogger(Converter.class);
-
-        private static final String DELIMITER = "&";
-
-        static String assemblyId(String... parts) {
-            return String.join(DELIMITER, parts);
-        }
-        
-        static String[] disassemblyId(String id) {
-            return id.split(DELIMITER);
-        }
-
-        static Filter convertFilter(io.connector.model.sensorthings.Filter ogcFilter) {
-            if (ogcFilter == null) {
-                throw new IllegalArgumentException("Unsupported filter attributes in filter expression '" + ogcFilter + "'.");
-            }
-
-            Filter afcFilter = new Filter();
-
-            for (io.connector.model.sensorthings.Filter.Expression expression : ogcFilter.getAddExpressions()) {
-                switch (expression.getAttribute()) {
-                    case "time":
-                    case "resultTime": {
-                        switch (expression.getOperator()) {
-                            case "lt": {
-                                afcFilter.endTime(Instant.parse(expression.getValue()));
-                            } break;
-                            case "gt": {
-                                afcFilter.startTime(Instant.parse(expression.getValue()));
-                            }break;
-                            default: throw new IllegalArgumentException(format(
-                                        "Unsupported operator '%s' in the filter expression '%s'.", expression.getOperator(), ogcFilter));
-                        }
-                    } break;
-                    default: throw new IllegalArgumentException(format(
-                                "Unsupported attribute '%s' in the filter expression '%s'.", expression.getAttribute(), ogcFilter));
-                }
-            }
-
-            return afcFilter;
-        }
-
-        static HistoricalLocation convertToHistoricalLocation(MultiSensor afcMultiSensor, AFCAggrLocation afcLocation, RequestUriComponent uriComponent) {
-            HistoricalLocation historicalLocation = new HistoricalLocation();
-            String locationId = assemblyId(afcMultiSensor.getResourceUrn(), afcLocation.measurement, afcLocation.observation.getTime());
-            String absoluteUrl = uriComponent.getGatewayUri();
-            historicalLocation.setId(locationId);
-            historicalLocation.setSelfLink(format("%s/HistoricalLocations(%s)", absoluteUrl, locationId));
-            historicalLocation.setLocationsNavigationLink(format("%s/HistoricalLocations(%s)/Locations", absoluteUrl, locationId));
-            historicalLocation.setThingNavigationLink(format("%s/HistoricalLocations(%s)/Thing", absoluteUrl, locationId));
-            historicalLocation.setTime(afcLocation.observation.getTime());
-            return historicalLocation;
-        }
-
-        static List<HistoricalLocation> convertToHistoricalLocation(MultiSensor afcMultiSensor, List<AFCAggrLocation> afcLocations, RequestUriComponent uriComponent) {
-            List<HistoricalLocation> historicalLocations = new ArrayList<>(afcLocations.size());
-            for (AFCAggrLocation afcLocation : afcLocations) {
-                historicalLocations.add(convertToHistoricalLocation(afcMultiSensor, afcLocation, uriComponent));
-            }
-            return historicalLocations;
-        }
-
-        static Location convertToLocation(MultiSensor afcMultiSensor, AFCAggrLocation afcLocation, RequestUriComponent uriComponent) {
-            Location location = new Location();
-            String locationId = assemblyId(afcMultiSensor.getResourceUrn(), afcLocation.measurement, afcLocation.observation.getTime());
-            String absoluteUrl = uriComponent.getGatewayUri();
-            location.setId(locationId);
-            location.setSelfLink(format("%s/Locations(%s)", absoluteUrl, locationId));
-            location.setHistoricalLocationsNavigationLink(format("%s/Locations(%s)/HistoricalLocations", absoluteUrl, locationId));
-            location.setName(afcMultiSensor.getResourceType());
-            location.setDescription(afcLocation.observation.getProvider());
-            location.setEncodingType("application/vnd.geo+json");
-
-            LocationInfo info = new LocationInfo();
-            info.setType("Feature");
-            location.setLocation(info);
-
-            Geometry geometry = new Geometry();
-            geometry.setType("Point");
-            io.connector.model.afarcloud.Observation afcObservation = afcLocation.observation;
-            geometry.setCoordinates(asList(afcObservation.getLongitude(), afcObservation.getLatitude(), afcObservation.getAltitude()));
-            info.setGeometry(geometry);
-
-            return location;
-        }
-
-        static Observation convertToObservation(ResourceMeasurement afcMeasurement, SensorTelemetry afcTelemetry, io.connector.model.afarcloud.Observation afcObservation, RequestUriComponent uriComponent) {
-            Observation observation = new Observation();
-            String observationId = assemblyId(afcMeasurement.getResource(), afcTelemetry.getMeasurement(), afcObservation.getTime());
-            String absoluteUrl = uriComponent.getGatewayUri();
-            observation.setId(observationId);
-            observation.setSelfLink(format("%s/Observations(%s)", absoluteUrl, observationId));
-            // observation.setFeatureOfInterestNavigationLink(format("Observations(%s)/FeatureOfInterest", observationId));
-            observation.setFeatureOfInterestNavigationLink("https://storage07-afarcloud.qa.pdmfc.com/storage/rest/registry/getAllObservationTypes");
-            observation.setDataStreamNavigationLink(format("%s/Observations(%s)/Datastream", absoluteUrl, observationId));
-            observation.setPhenomenonTime(afcObservation.getTime());
-            observation.setResultTime(afcObservation.getTime());
-            observation.setResult(afcObservation.getValue());
-            return observation;
-        }
-
-        static List<Observation> convertToObservations(ResourceMeasurement afcMeasurement, SensorTelemetry afcTelemetry, RequestUriComponent uriComponent) {
-            List<Observation> ogcObservations = new ArrayList<>(afcTelemetry.getObservations().size());
-            for (io.connector.model.afarcloud.Observation afcObservation : afcTelemetry.getObservations()) {
-                ogcObservations.add(convertToObservation(afcMeasurement, afcTelemetry, afcObservation, uriComponent));
-            }
-            return ogcObservations;
-        }
-
-        static ObservedProperty convertToObservedProperty(MultiSensor afcMultiSensor, MultiSensor.SensorSchema afcSensor, RequestUriComponent uriComponent) {
-            Objects.requireNonNull(afcMultiSensor);
-            Objects.requireNonNull(afcSensor);
-            Objects.requireNonNull(uriComponent);
-
-            ObservedProperty observedProperty = new ObservedProperty();
-            String observedPropertyId = assemblyId(afcMultiSensor.getResourceUrn(), afcSensor.getObservedProperty());
-            String absoluteUrl = uriComponent.getGatewayUri();
-            observedProperty.setId(observedPropertyId);
-            observedProperty.setSelfLink(format("%s/ObservedProperties(%s)", absoluteUrl, observedPropertyId));
-            observedProperty.setDataStreamNavigationLink(format("%s/ObservedProperties(%s)/Datastream", absoluteUrl, observedPropertyId));
-            observedProperty.setName(afcSensor.getObservedProperty());
-            observedProperty.setDescription(afcSensor.getObservedProperty());
-            observedProperty.setDefinition(afcSensor.getUom());
-            return observedProperty;
-        }
-
-        static Thing convertToThing(MultiSensor afcMultiSensor, RequestUriComponent uriComponent) {
-            Objects.requireNonNull(afcMultiSensor);
-            Objects.requireNonNull(uriComponent);
-
-            Thing thing = new Thing();
-            String thingId = afcMultiSensor.getResourceUrn();
-            String absoluteUrl = uriComponent.getGatewayUri();
-            thing.setId(thingId);
-            thing.setSelfLink(format("%s/Things(%s)", absoluteUrl, thingId));
-            thing.setLocationsNavigationLink(format("%s/Things(%s)/Locations", absoluteUrl, thingId));
-            thing.setDataStreamNavigationLink(format("%s/Things(%s)/Datastreams", absoluteUrl, thingId));
-            thing.setHistoricalLocationsNavigationLink(format("%s/Things(%s)/HistoricalLocations", absoluteUrl, thingId));
-            thing.setName(afcMultiSensor.getResourceType());
-            thing.setDescription(afcMultiSensor.getResourceType());
-            thing.setProperties(null);
-            return thing;
-        }
-
-        static List<Thing> convertToThing(List<MultiSensor> afcMultiSensors, RequestUriComponent uriComponent) {
-            List<Thing> ogcThings = new ArrayList<>(afcMultiSensors.size());
-            for (MultiSensor afcMultiSensor : afcMultiSensors) {
-                ogcThings.add(convertToThing(afcMultiSensor, uriComponent));
-            }
-            return ogcThings;
-        }
-
-        static Datastream convertToDataStream(MultiSensor afcMultiSensor, MultiSensor.SensorSchema afcSensor, SensorTelemetry telemetry, RequestUriComponent uriComponent) {
-            Objects.requireNonNull(afcMultiSensor);
-            Objects.requireNonNull(afcSensor);
-            Objects.requireNonNull(uriComponent);
-
-            Datastream datastream = new Datastream();
-            String dataStreamId = assemblyId(afcMultiSensor.getResourceUrn(), afcSensor.getObservedProperty());
-            String absoluteUrl = uriComponent.getGatewayUri();
-            datastream.setId(dataStreamId);
-            datastream.setSelfLink(format("%s/Datastreams(%s)", absoluteUrl, dataStreamId));
-            datastream.setThingNavigationLink(format("%s/Datastreams(%s)/Thing", absoluteUrl, dataStreamId));
-            datastream.setSensorNavigationLink(format("%s/Datastreams(%s)/Sensor", absoluteUrl, dataStreamId));
-            datastream.setObservedPropertyNavigationLink(format("%s/Datastreams(%s)/ObservedProperty", absoluteUrl, dataStreamId));
-            datastream.setObservationsNavigationLink(format("%s/Datastreams(%s)/Observations", absoluteUrl, dataStreamId));
-            datastream.setName(afcSensor.getObservedProperty());
-            datastream.setDescription(afcSensor.getObservedProperty());
-
-            UnitOfMeasurement uom = new UnitOfMeasurement();
-            uom.setName(afcSensor.getObservedProperty());
-            uom.setSymbol("");
-            uom.setDefinition(afcSensor.getUom());
-            datastream.setUnitOfMeasurement(uom);
-
-            datastream.setObservationType("http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement");
-
-            Geometry geometry = new Geometry();
-            geometry.setType("Point");
-            geometry.setCoordinates(asList(afcMultiSensor.getLongitude(), afcMultiSensor.getLatitude()));
-            datastream.setObservedArea(geometry);
-
-            String startDate = "<none>";
-            String endDate = telemetry == null || telemetry.getObservations().isEmpty() ? "<none>" : telemetry.getObservations().get(0).getTime();
-            String time = startDate + "/" + endDate;
-            datastream.setPhenomenonTime(time);
-            datastream.setResultTime(time);
-
-            return datastream;
-        }
-
-        static List<Datastream> convertToDataStream(MultiSensor afcMultiSensor, ResourceMeasurement measurement, RequestUriComponent uriComponent) {
-            Objects.requireNonNull(afcMultiSensor);
-            Objects.requireNonNull(uriComponent);
-
-            List<Datastream> datastreams = new ArrayList<>(afcMultiSensor.getObservations().size());
-
-            Map<String, SensorTelemetry> sensors = new HashMap<>();
-            if (measurement != null) {
-                for (SensorTelemetry telemetry : measurement.getMeasurements()) {
-                    sensors.put(telemetry.getMeasurement(), telemetry);
-                }
-            }
-
-            for (MultiSensor.SensorSchema sensor : afcMultiSensor.getObservations()) {
-                datastreams.add(convertToDataStream(afcMultiSensor, sensor, sensors.get(sensor.getObservedProperty()), uriComponent));
-            }
-
-            return datastreams;
-        }
-
-        static Sensor convertToSensor(MultiSensor afcMultiSensor, RequestUriComponent uriComponent) {
-            Objects.requireNonNull(afcMultiSensor);
-            Objects.requireNonNull(uriComponent);
-
-            String afcDomain = "https://storage07-afarcloud.qa.pdmfc.com/storage/rest";
-
-            Sensor sensor = new Sensor();
-            String thingId = afcMultiSensor.getResourceUrn();
-            String absoluteUrl = uriComponent.getGatewayUri();
-            sensor.setId(thingId);
-            sensor.setSelfLink(format("%s/Sensors(%s)", absoluteUrl, thingId));
-            sensor.setDataStreamNavigationLink(format("%s/Sensors(%s)/Datastreams", absoluteUrl, thingId));
-            sensor.setName(afcMultiSensor.getResourceType());
-            sensor.setDescription(afcMultiSensor.getResourceType());
-            sensor.setEncodingType("application/json");
-            sensor.setMetadata(format("%s/registry/getSensor/%s", afcDomain, afcMultiSensor.getResourceId()));
-
-            return sensor;
-        }
-
-        static MultiSimpleObservation convertToMultiSimpleObservation(ObservationInsert ogcObservation) {
-            String [] idPartsAfc = disassemblyId(ogcObservation.getDatastreamId());
-            String resourceUrn = idPartsAfc[0], observedProperty = idPartsAfc[1];
-            String [] urnPartsAfc = resourceUrn.split(":");
-            String resourceId = urnPartsAfc[urnPartsAfc.length-1];
-
-            MultiSimpleObservation multiSimpleObservation = new MultiSimpleObservation();
-            multiSimpleObservation.setResourceId(resourceId);
-
-            SimpleObservation simpleObservation = new SimpleObservation();
-            simpleObservation.setObservedProperty(observedProperty);
-            simpleObservation.setResultTime(ogcObservation.getPhenomenonTime().toEpochSecond());
-            simpleObservation.setResult(ogcObservation.getResult());
-
-            multiSimpleObservation.setObservations(singletonList(simpleObservation));
-
-            return multiSimpleObservation;
-        }
-
-        static MultiSensor convertToMultiSensor(ThingInsert ogcThing) {
-            String afcResourceType = ogcThing.getName();
-            if (!afcResourceType.equals(ogcThing.getDescription())) {
-                throw new IllegalArgumentException("Attribute 'description' has to be equal with 'name'. Contains 'resourceType' attribute from AFC.");
-            }
-            if (ogcThing.getLocations().size() != 1) {
-                throw new IllegalArgumentException("Attribute 'Locations' has to contain one location.");
-            }
-            Location ogcLocation = ogcThing.getLocations().get(0);
-            if (!afcResourceType.equals(ogcLocation.getName())) {
-                throw new IllegalArgumentException("Attribute 'Location/name' has to be equal with 'name'. Contains 'resourceType attribute from AFC.");
-            }
-            LocationInfo ogcLocationInfo = ogcLocation.getLocation();
-            String ogcLocationInfoType = ogcLocationInfo.getType();
-            if (!ogcLocationInfoType.equals("Feature")) {
-                throw new IllegalArgumentException("Allowed only 'Feature' type of location.");
-            }
-            Geometry ogcLocationGeometry = ogcLocationInfo.getGeometry();
-            if (!ogcLocationGeometry.getType().equals("Point")) {
-                throw new IllegalArgumentException("Allowed only 'Point' type of geometry.");
-            }
-            List<Double> ogcCoordinates = ogcLocationGeometry.getCoordinates();
-            if (ogcCoordinates.size() != 3) {
-                throw new IllegalArgumentException("Coordinates of location have to be in following format [longitude, latitude, altitude].");
-            }
-
-            double afcLongitude = ogcCoordinates.get(0);
-            double afcLatitude = ogcCoordinates.get(1);
-            double afcAltitude = ogcCoordinates.get(2);
-
-            List<DatastreamInsert> ogcDatastreams = ogcThing.getDatastreams();
-            List<MultiSensor.SensorSchema> afcSensorSchemas = new ArrayList<>(ogcDatastreams.size());
-            for (DatastreamInsert ogcDatastream : ogcDatastreams) {
-                String afcObservedProperty = ogcDatastream.getName();
-                if (!afcObservedProperty.equals(ogcDatastream.getDescription())) {
-                    throw new IllegalArgumentException("Attribute 'Datastream/description' has to be equal with 'Datastream/name'. Contains 'observedProperty attribute from AFC.");
-                }
-                UnitOfMeasurement ogcUnitOfMeasurement = ogcDatastream.getUnitOfMeasurement();
-                String afcUom = ogcUnitOfMeasurement.getDefinition();
-
-                String ogcObservationType = ogcDatastream.getObservationType();
-                if (!ogcObservationType.equals("http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement")) {
-                    throw new IllegalArgumentException("For the attribute 'Datastream/observationType' is allowed following values [http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement].");
-                }
-
-                Geometry ogcObservedArea = ogcDatastream.getObservedArea();
-                if (!ogcObservedArea.getType().equals("Point")) {
-                    throw new IllegalArgumentException("Allowed only 'Point' type of observedArea for the datastream '"+afcObservedProperty+"'.");
-                }
-                List<Double> ogcObcAreaCoordinates = ogcObservedArea.getCoordinates();
-                if (ogcObcAreaCoordinates.size() != 3) {
-                    throw new IllegalArgumentException("Coordinates of location for the datastream '"+afcObservedProperty+"' have to be in following format [longitude, latitude, altitude].");
-                }
-                if (!(ogcObcAreaCoordinates.get(0).equals(afcLongitude) || ogcObcAreaCoordinates.get(1).equals(afcLatitude) || ogcObcAreaCoordinates.get(2).equals(afcAltitude))) {
-                    throw new IllegalArgumentException("Coordinates of the observedArea have to be same as the last location.");
-                }
-
-                ObservedProperty ogcObservedProperty = ogcDatastream.getObservedProperty();
-                if (!ogcObservedProperty.getName().equals(afcObservedProperty)) {
-                    throw new IllegalArgumentException("Name of ObservedProperty has to be same as the name of datastream.");
-                }
-                if (!ogcObservedProperty.getDescription().equals(afcObservedProperty)) {
-                    throw new IllegalArgumentException("Description of ObservedProperty has to be same as the name of the datastream.");
-                }
-                if (!ogcObservedProperty.getDefinition().equals(afcUom)) {
-                    throw new IllegalArgumentException("Definition of ObservedProperty has to be same as the definition of unit measurement.");
-                }
-
-                SensorInsert ogcSensor = ogcDatastream.getSensor();
-                if (!ogcSensor.getName().equals(afcResourceType)) {
-                    throw new IllegalArgumentException("Name of sensor has to be same as the name of thing.");
-                }
-                if (!ogcSensor.getDescription().equals(afcResourceType)) {
-                    throw new IllegalArgumentException("Description of sensor has to be same as the name of thing.");
-                }
-                if (!ogcSensor.getEncodingType().equals("application/json")) {
-                    throw new IllegalArgumentException("For the attribute 'datastream/sensor/encodingType is allowed only 'application/json' type.");
-                }
-
-                MultiSensor.SensorSchema afcSensor = new MultiSensor.SensorSchema();
-                afcSensor.setObservedProperty(afcObservedProperty);
-                afcSensor.setUom(afcUom);
-                afcSensor.setMin_value(Double.MIN_VALUE);   // TODO set min_value
-                afcSensor.setMax_value(Double.MAX_VALUE);   // TODO set max_value
-                afcSensor.setAccuracy(0.1);                 // TODO set accuracy
-                afcSensorSchemas.add(afcSensor);
-            }
-
-            if (ogcThing.getProperties() == null) {
-                final String[] propAttrs = new String[]{"resourceUrn","resourceId", "supportedProtocol", "hardwareVersion", "softwareVersion", "firmwareVersion", "preprocessing", "pythonScript"};
-                throw new IllegalArgumentException("Attribute 'properties' is required and has to contain following attributes " + Arrays.toString(propAttrs) + ".");
-            }
-            Map<String, Object> ogcProperties = ogcThing.getProperties();
-
-            String afcResourceId = (String) ogcProperties.get("resourceId");
-            if (afcResourceId == null) {
-                throw new IllegalArgumentException("Attribute 'properties/resourceId' is required.");
-            }
-            String afcResourceUrn = (String) ogcProperties.get("resourceUrn");
-            if (afcResourceUrn == null) {
-                throw new IllegalArgumentException("Attribute 'properties/resourceUrn' is required.");
-            }
-            String afcSupportedProtocol = (String) ogcProperties.get("supportedProtocol");
-            if (afcSupportedProtocol == null) {
-                throw new IllegalArgumentException("Attribute 'properties/supportedProtocol' is required.");
-            }
-            String afcHardwareVersion = (String) ogcProperties.get("hardwareVersion");
-            if (afcHardwareVersion == null) {
-                throw new IllegalArgumentException("Attribute 'properties/hardwareVersion' is required.");
-            }
-            String afcSoftwareVersion = (String) ogcProperties.get("softwareVersion");
-            if (afcSoftwareVersion == null) {
-                throw new IllegalArgumentException("Attribute 'properties/softwareVersion' is required.");
-            }
-            String afcFirmwareVersion = (String) ogcProperties.get("firmwareVersion");
-            if (afcFirmwareVersion == null) {
-                throw new IllegalArgumentException("Attribute 'properties/firmwareVersion' is required.");
-            }
-            String afcPythonScript = (String) ogcProperties.get("pythonScript");
-            if (afcPythonScript == null) {
-                throw new IllegalArgumentException("Attribute 'properties/pythonScript' is required.");
-            }
-            Boolean afcPreprocessing = (Boolean) ogcProperties.get("preprocessing");
-            if (afcPreprocessing == null) {
-                throw new IllegalArgumentException("Attribute 'properties/preprocessing' is required.");
-            }
-
-            MultiSensor afcMultiSensor = new MultiSensor();
-            afcMultiSensor.setResourceId(afcResourceId);
-            afcMultiSensor.setResourceType(afcResourceType);
-            afcMultiSensor.setResourceUrn(afcResourceUrn);
-            afcMultiSensor.setLongitude(afcLongitude);
-            afcMultiSensor.setLatitude(afcLatitude);
-            afcMultiSensor.setAltitude(afcAltitude);
-            afcMultiSensor.setPreprocessing(afcPreprocessing);
-            afcMultiSensor.setPythonScript(afcPythonScript);
-            afcMultiSensor.setObservations(afcSensorSchemas);
-            afcMultiSensor.setSupportedProtocol(afcSupportedProtocol);
-            afcMultiSensor.setHardwareVersion(afcHardwareVersion);
-            afcMultiSensor.setSoftwareVersion(afcSoftwareVersion);
-            afcMultiSensor.setFirmwareVersion(afcFirmwareVersion);
-
-            return afcMultiSensor;
-        }
-    }
-}
+// Copyright (c) 2020 UWB & LESP.
+// The UWB & LESP license this file to you under the MIT license.
+
+package io.connector.module.afarcloud.gateway;
+
+import io.connector.core.AbstractGateway;
+import io.connector.core.http.RequestUriComponent;
+import io.connector.model.afarcloud.*;
+import io.connector.model.sensorthings.*;
+import io.connector.model.sensorthings.Location;
+import io.connector.model.sensorthings.Observation;
+import io.connector.module.afarcloud.AFCHttpClient;
+import io.connector.module.afarcloud.Filter;
+import io.vertx.core.json.Json;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Route;
+import io.vertx.ext.web.handler.BodyHandler;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Instant;
+import java.util.*;
+import java.util.function.Supplier;
+
+import static io.connector.core.http.HttpContentType.APPLICATION_JSON;
+import static io.connector.core.http.HttpHeader.CONTENT_TYPE;
+import static io.connector.core.AddressPath.Creator.create;
+import static io.vertx.core.json.Json.encode;
+import static java.lang.Math.sqrt;
+import static java.lang.StrictMath.pow;
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static java.util.Optional.ofNullable;
+
+/**
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class OGCSensorThingsGateway extends AbstractGateway {
+
+    private final static Logger logger = LogManager.getLogger(OGCSensorThingsGateway.class);
+
+    /** Attribute of a client that accesses to the AFarCloud system.  */
+    private final AFCHttpClient client;
+
+    /**
+     * Public constructor of the class that initializes final attributes.
+     * @param id - identifier of the gateway
+     * @param client - client allows access to the AFarCloud system
+     */
+    public OGCSensorThingsGateway(String id, AFCHttpClient client) {
+        super(id);
+        this.client = client;
+    }
+
+    @Override
+    protected void run() {
+
+        router().post().handler(BodyHandler.create()).handler(ctx -> {
+            String contentType = ctx.request().getHeader(CONTENT_TYPE);
+            if (!contentType.equals(APPLICATION_JSON)) {
+                ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).setStatusCode(415)
+                        .end(new JsonObject().put("message", String.format(
+                                "Unsupported content type. Use one of these [%s].", APPLICATION_JSON
+                        )).encode()
+                );
+            }
+            ctx.next();
+        });
+
+        router().get(create("Things")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            List<MultiSensor> afcMultiSensors = client.getAllSensors();
+            List<Thing> ogcThings = Converter.convertToThing(afcMultiSensors, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(encode(ogcThings));
+        });
+
+        router().get(create("Things(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String resourceUrn = ctx.pathParam("id"); // resourceUrn
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            Thing ogcThing = Converter.convertToThing(afcMultiSensor, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcThing.encode());
+        });
+
+        router().get(create("Datastreams(:id)/Thing")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            String[] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0];
+            ctx.reroute(format("%s/Things(%s)", uriComponent.getGatewayPath(), resourceUrn));
+        });
+
+        router().get(create("HistoricalLocations(:id)/Thing")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
+            String [] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0];
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            Thing ogcThing = Converter.convertToThing(afcMultiSensor, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcThing.encode());
+        });
+
+        router().get(create("Datastreams(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            String[] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0], observedProperty = idCmp[1];
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            List<ResourceMeasurement> afcMeasurements = client.getLatestObservationsBySensor(new Filter()
+                    .limit(1).entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
+            );
+            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Datastream with @iot.id \"" + id + "\".");
+
+            Optional<MultiSensor.SensorSchema> afcSensorOpt = afcMultiSensor.getObservations().stream().filter(s -> s.getObservedProperty().equals(observedProperty)).findFirst();
+            MultiSensor.SensorSchema afcSensor = afcSensorOpt.orElseThrow(exception);
+
+            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
+            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
+
+            Optional<SensorTelemetry> afcTelemetryOpt = ofNullable(afcMeasurement.getMeasurements().size() == 1 ? afcMeasurement.getMeasurements().get(0) : null);
+            SensorTelemetry afcTelemetry = afcTelemetryOpt.orElseThrow(exception);
+
+            Datastream datastream = Converter.convertToDataStream(afcMultiSensor, afcSensor, afcTelemetry, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(datastream.encode());
+        });
+
+        router().get(create("Things(:id)/Datastreams")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String resourceUrn = ctx.pathParam("id"); // resourceUrn
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            List<ResourceMeasurement> afcMeasurements = client.getLatestObservationsBySensor(new Filter()
+                    .limit(1)
+                    .entityNames(afcMultiSensor.getResourceId())
+            );
+            ResourceMeasurement afcMeasurement = afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null;
+            List<Datastream> ogcDataStream = Converter.convertToDataStream(afcMultiSensor, afcMeasurement, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(encode(ogcDataStream));
+        });
+
+        router().get(create("Sensors(:id)/Datastreams")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String resourceUrn = ctx.pathParam("id"); // resourceUrn
+            ctx.reroute(format("%s/Things(%s)/Datastreams", uriComponent.getGatewayPath(), resourceUrn));
+        });
+
+        router().get(create("ObservedProperties(:id)/Datastream")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            ctx.reroute(format("%s/Datastreams(%s)", uriComponent.getGatewayPath(), id));
+        });
+
+        router().get(create("Observations(:id)/Datastream")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resource + measurement + time
+            String[] idCmp = Converter.disassemblyId(id);
+            String resource = idCmp[0], measurement = idCmp[1];
+            MultiSensor afcMultiSensor = client.getSensorByResourceId(resource);
+            String resourceUrn = afcMultiSensor.getResourceUrn();
+            ctx.reroute(format("%s/Datastreams(%s)", uriComponent.getGatewayPath(), Converter.assemblyId(resourceUrn, measurement)));
+        });
+
+        router().get(create("Sensors(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String resourceUrn = ctx.pathParam("id"); // resourceUrn
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            Sensor ogcSensor = Converter.convertToSensor(afcMultiSensor, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcSensor.encode());
+        });
+
+        router().get(create("Datastreams(:id)/Sensor")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            String[] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0];
+            ctx.reroute(format("%s/Sensors(%s)", uriComponent.getGatewayPath(), resourceUrn));
+        });
+
+        router().get(create("ObservedProperties(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            String[] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0], observedProperty = idCmp[1];
+            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Datastream with @iot.id \"" + id + "\".");
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            Optional<MultiSensor.SensorSchema> afcSensorOpt = afcMultiSensor.getObservations().stream().filter(s -> s.getObservedProperty().equals(observedProperty)).findFirst();
+            MultiSensor.SensorSchema afcSensor = afcSensorOpt.orElseThrow(exception);
+            ObservedProperty property = Converter.convertToObservedProperty(afcMultiSensor, afcSensor, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(property.encode());
+        });
+
+        router().get(create("Datastreams(:id)/ObservedProperty")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            ctx.reroute(format("%s/ObservedProperties(%s)", uriComponent.getGatewayPath(), id));
+        });
+
+        router().get(create("Observations(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resource + measurement + time
+            String[] idCmp = Converter.disassemblyId(id);
+            String resource = idCmp[0], measurement = idCmp[1], time = idCmp[2];
+
+            Instant timestamp = Instant.parse(time);
+
+            List<ResourceMeasurement> measurements = client.getObservationsBySensor(new Filter()
+                    .limit(1).order("ASC").entityNames(resource).measurements(measurement)
+                    .startTime(timestamp).endTime(timestamp.plusSeconds(60))
+            );
+            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException(format(
+                    "Can not find Observation with @iot.id '%s'.", id));
+
+            Optional<ResourceMeasurement> measurementOpt = ofNullable(measurements.size() == 1 ? measurements.get(0) : null);
+            ResourceMeasurement afcMeasurement = measurementOpt.orElseThrow(exception);
+
+            Optional<SensorTelemetry> telemetryOpt = ofNullable(afcMeasurement.getMeasurements().size() == 1 ? afcMeasurement.getMeasurements().get(0) : null);
+            SensorTelemetry afcTelemetry = telemetryOpt.orElseThrow(exception);
+
+            Optional<io.connector.model.afarcloud.Observation> observationOpt = ofNullable(afcTelemetry.getObservations().size() > 0 ? afcTelemetry.getObservations().get(0) : null);
+            io.connector.model.afarcloud.Observation afcObservation = observationOpt.orElseThrow(exception);
+
+            Observation ogcObservation = Converter.convertToObservation(afcMeasurement, afcTelemetry, afcObservation, uriComponent);
+            ctx.response().end(ogcObservation.encode());
+        });
+
+        router().get(create("Datastreams(:id)/Observations")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty
+            String filterParam = ctx.request().getParam(Params.FILTER);
+
+            io.connector.model.sensorthings.Filter filter = null;
+            try {
+                filter = io.connector.model.sensorthings.Filter.parse(filterParam);
+            } catch (RuntimeException e) {
+                ctx.response().setStatusCode(501).end(new JsonObject()
+                        .put("message", e.getMessage()).encode()); return;
+            }
+
+            String [] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0], observedProperty = idCmp[1];
+
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+
+            List<ResourceMeasurement> measurements;
+            if (filter.isExists()) {
+                Filter afcFilter;
+                try {
+                    afcFilter = Converter.convertFilter(filter);
+                } catch (RuntimeException e) {
+                    ctx.response().setStatusCode(501).end(new JsonObject()
+                            .put("message", e.getMessage()).encode()); return;
+                }
+                afcFilter.entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty);
+                measurements = client.getObservationsBySensor(afcFilter);
+            } else {
+                measurements = client.getLatestObservationsBySensor(new Filter()
+                        .limit(1).entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
+                );
+            }
+
+            if (measurements.isEmpty()) {
+                ctx.response().end(new JsonArray().encode());
+            } else {
+                final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Datastream with @iot.id \"" + id + "\".");
+
+                Optional<ResourceMeasurement> measurementOpt = ofNullable(measurements.size() == 1 ? measurements.get(0) : null);
+                ResourceMeasurement afcMeasurement = measurementOpt.orElseThrow(exception);
+
+                Optional<SensorTelemetry> telemetryOpt = ofNullable(afcMeasurement.getMeasurements().size() == 1 ? afcMeasurement.getMeasurements().get(0) : null);
+                SensorTelemetry afcTelemetry = telemetryOpt.orElseThrow(exception);
+
+                List<Observation> ogcObservations = Converter.convertToObservations(afcMeasurement, afcTelemetry, uriComponent);
+                ctx.response().end(encode(ogcObservations));
+            }
+        });
+
+        router().get(create("FeaturesOfInterest(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id");
+            FeatureOfInterest featureOfInterest = new FeatureOfInterest();
+            featureOfInterest.setId(id);
+            featureOfInterest.setSelfLink("https://storage07-afarcloud.qa.pdmfc.com/storage/rest/registry/getAllObservationTypes");
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(featureOfInterest.encode());
+        });
+
+        router().get(create("Locations(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
+            String [] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0], observedProperty = idCmp[1], time = idCmp[2];
+
+            Instant startTime = Instant.parse(time);
+            Instant endTime = startTime.plusSeconds(60);
+
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            List<ResourceMeasurement> afcMeasurements = client.getObservationsBySensor(new Filter()
+                    .startTime(startTime).endTime(endTime)
+                    .entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
+            );
+            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Location with @iot.id \"" + resourceUrn + "\".");
+
+            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
+            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
+
+            AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
+            Optional<AFCAggrLocation> afcLastLocationOpt = ofNullable(afcLocations.getLast());
+            AFCAggrLocation afcLastLocation = afcLastLocationOpt.orElseThrow(exception);
+
+            Location ogcLocation = Converter.convertToLocation(afcMultiSensor, afcLastLocation, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcLocation.encode());
+        });
+
+        router().get(create("Things(:id)/Locations")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String resourceUrn = ctx.pathParam("id");  // resourceUrn
+
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            List<ResourceMeasurement> afcMeasurements = client.getLatestObservationsBySensor(new Filter()
+                    .limit(1).entityNames(afcMultiSensor.getResourceId())
+            );
+            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Thing with @iot.id \"" + resourceUrn + "\".");
+
+            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
+            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
+
+            AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
+            Optional<AFCAggrLocation> afcLastLocationOpt = ofNullable(afcLocations.getFirst());
+            AFCAggrLocation afcLastLocation = afcLastLocationOpt.orElseThrow(exception);
+
+            Location ogcLocation = Converter.convertToLocation(afcMultiSensor, afcLastLocation, uriComponent);
+            List<Location> ogcLocations = singletonList(ogcLocation);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(encode(ogcLocations));
+        });
+
+        router().get(create("HistoricalLocations(:id)/Locations")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
+            ctx.reroute(format("%s/Locations(%s)", uriComponent.getGatewayPath(), id));
+        });
+
+        router().get(create("HistoricalLocations(:id)")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
+
+            String [] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0], observedProperty = idCmp[1], time = idCmp[2];
+
+            Instant startTime = Instant.parse(time);
+            Instant endTime = startTime.plusSeconds(60);
+
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            List<ResourceMeasurement> afcMeasurements = client.getObservationsBySensor(new Filter()
+                    .startTime(startTime).endTime(endTime)
+                    .entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
+            );
+            final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find Location with @iot.id \"" + resourceUrn + "\".");
+
+            Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
+            ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
+
+            AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
+            Optional<AFCAggrLocation> afcLastLocationOpt = ofNullable(afcLocations.getLast());
+            AFCAggrLocation afcLocation = afcLastLocationOpt.orElseThrow(exception);
+
+            HistoricalLocation ogcLocation = Converter.convertToHistoricalLocation(afcMultiSensor, afcLocation, uriComponent);
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON).end(ogcLocation.encode());
+        });
+
+        router().get(create("Things(:id)/HistoricalLocations")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String resourceUrn = ctx.pathParam("id"); // resourceUrn
+            String filterParam = ctx.request().getParam(Params.FILTER);
+
+            io.connector.model.sensorthings.Filter filter = null;
+            try {
+                filter = io.connector.model.sensorthings.Filter.parse(filterParam);
+            } catch (RuntimeException e) {
+                ctx.response().setStatusCode(501).end(new JsonObject()
+                        .put("message", e.getMessage()).encode()); return;
+            }
+
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+            List<ResourceMeasurement> afcMeasurements;
+            if (filter.isExists()) {
+                Filter afcFilter;
+                try {
+                    afcFilter = Converter.convertFilter(filter);
+                } catch (RuntimeException e) {
+                    ctx.response().setStatusCode(501).end(new JsonObject()
+                            .put("message", e.getMessage()).encode()); return;
+                }
+                afcFilter.entityNames(afcMultiSensor.getResourceId());
+                afcMeasurements = client.getObservationsBySensor(afcFilter);
+            } else {
+                afcMeasurements = client.getLatestObservationsBySensor(new Filter()
+                        .limit(1).entityNames(afcMultiSensor.getResourceId())
+                );
+            }
+
+            if (afcMeasurements.isEmpty()) {
+                ctx.response().end(new JsonArray().encode());
+            } else {
+                final Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException("Can not find HistoricalLocations of the Thing with @iot.id \"" + resourceUrn + "\".");
+
+                Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
+                ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
+
+                AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
+
+                List<HistoricalLocation> locations = Converter.convertToHistoricalLocation(afcMultiSensor, afcLocations.getList(), uriComponent);
+                ctx.response().end(encode(locations));
+            }
+        });
+
+        router().get(create("Locations(:id)/HistoricalLocations")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+            RequestUriComponent uriComponent = parseUriToComponents(ctx.request());
+            String id = ctx.pathParam("id"); // resourceUrn + observedProperty + time
+            String filterParam = ctx.request().getParam(Params.FILTER);
+
+            io.connector.model.sensorthings.Filter filter = null;
+            try {
+                filter = io.connector.model.sensorthings.Filter.parse(filterParam);
+            } catch (RuntimeException e) {
+                ctx.response().setStatusCode(501).end(new JsonObject()
+                        .put("message", e.getMessage()).encode()); return;
+            }
+
+            String [] idCmp = Converter.disassemblyId(id);
+            String resourceUrn = idCmp[0], observedProperty = idCmp[1], time = idCmp[2];
+
+            final Supplier<IllegalArgumentException> exception =
+                    () -> new IllegalArgumentException("Can not find HistoricalLocations of the Thing with @iot.id \"" + resourceUrn + "\".");
+
+            MultiSensor afcMultiSensor = client.getSensorByResourceUrn(resourceUrn);
+
+            Filter afcFilter;
+            if (filter.isExists()) {
+                try {
+                    afcFilter = Converter.convertFilter(filter);
+                } catch (RuntimeException e) {
+                    ctx.response().setStatusCode(501).end(new JsonObject()
+                            .put("message", e.getMessage()).encode()); return;
+                }
+                afcFilter.entityNames(afcMultiSensor.getResourceId());
+            } else {
+                Instant startTime = Instant.parse(time);
+                Instant endTime = startTime.plusSeconds(60*60); // 1h
+
+                afcFilter = new Filter().startTime(startTime).endTime(endTime);
+            }
+
+            List<ResourceMeasurement> afcMeasurements = client.getObservationsBySensor(afcFilter
+                    .entityNames(afcMultiSensor.getResourceId()).measurements(observedProperty)
+            );
+
+            if (afcMeasurements.isEmpty()) {
+                ctx.response().end(new JsonArray().encode());
+            } else {
+
+                Optional<ResourceMeasurement> afcMeasurementOpt = ofNullable(afcMeasurements.size() == 1 ? afcMeasurements.get(0) : null);
+                ResourceMeasurement afcMeasurement = afcMeasurementOpt.orElseThrow(exception);
+
+                AFCLocationList afcLocations = AFCLocationUtils.sort(afcMeasurement);
+
+                List<HistoricalLocation> locations = Converter.convertToHistoricalLocation(afcMultiSensor, afcLocations.getList(), uriComponent);
+                ctx.response().end(encode(locations));
+            }
+        });
+
+        router().post(create("Observations")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+
+            JsonObject bodyJson = ctx.getBodyAsJson();
+            if (bodyJson.isEmpty()) {
+                ctx.response().setStatusCode(204).send(); return;
+            }
+            try {
+                ObservationInsert ogcObservation = ObservationInsert.parse(bodyJson);
+                MultiSimpleObservation afcObservation = Converter.convertToMultiSimpleObservation(ogcObservation);
+                client.uploadAggregatedMeasurements(singletonList(afcObservation));
+            } catch (RuntimeException e) {
+                ctx.response().setStatusCode(501).end(new JsonObject()
+                        .put("message", e.getMessage()).encode()); return;
+            }
+            ctx.response().setStatusCode(201).send();
+        });
+
+        router().post(create("Things")).handler(ctx -> {
+            logger.info("Handling a request: {}.", ctx.request().absoluteURI());
+            ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
+
+            ThingInsert ogcThing = ThingInsert.parse(ctx.getBodyAsJson());
+            MultiSensor afcMultiSensor = Converter.convertToMultiSensor(ogcThing);
+
+            ctx.response().end(Json.encode(afcMultiSensor));
+        });
+    }
+
+    private static class AFCAggrLocation implements Comparable<AFCAggrLocation> {
+        private final String measurement;
+        private final io.connector.model.afarcloud.Observation observation;
+
+        public AFCAggrLocation(String measurement, io.connector.model.afarcloud.Observation observation) {
+            this.measurement = measurement;
+            this.observation = observation;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            AFCAggrLocation that = (AFCAggrLocation) o;
+            return this.compareTo(that) == 0;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(measurement, observation);
+        }
+
+        @Override
+        public int compareTo(AFCAggrLocation o) {
+            io.connector.model.afarcloud.Observation o1 = this.observation;
+            io.connector.model.afarcloud.Observation o2 = o.observation;
+
+            Instant o1Time = Instant.parse(o1.getTime());
+            Instant o2Time = Instant.parse(o2.getTime());
+
+            if (o1Time.equals(o2Time)) {
+                Double o1Dist = sqrt(pow(o1.getLongitude(), 2) + pow(o1.getLatitude(), 2) + pow(o1.getAltitude(), 2));
+                Double o2Dist = sqrt(pow(o2.getLongitude(), 2) + pow(o2.getLatitude(), 2) + pow(o2.getAltitude(), 2));
+                return o1Dist.compareTo(o2Dist);
+            } else {
+                return o1Time.compareTo(o2Time);
+            }
+        }
+    }
+
+    private static class AFCLocationList {
+        private final List<AFCAggrLocation> list;
+
+        AFCLocationList() {
+            this.list = new ArrayList<>();
+        }
+
+        public void sort() {
+            Collections.sort(list);
+        }
+
+        public void add(AFCAggrLocation location) {
+            list.add(location);
+        }
+
+        public AFCAggrLocation getFirst() {
+            return list.isEmpty() ? null : list.get(0);
+        }
+
+        public AFCAggrLocation getLast() {
+            return list.isEmpty() ? null : list.get(list.size()-1);
+        }
+
+        public AFCAggrLocation get(int index) {
+            return list.get(index);
+        }
+
+        public List<AFCAggrLocation> getList() {
+            return list;
+        }
+    }
+
+    private static class AFCLocationUtils {
+
+        static AFCLocationList sort(ResourceMeasurement afcMeasurement) {
+            Objects.requireNonNull(afcMeasurement);
+
+            AFCLocationList aggrLocations = new AFCLocationList();
+            for (SensorTelemetry measurement : afcMeasurement.getMeasurements()) {
+                for (io.connector.model.afarcloud.Observation observation : measurement.getObservations()) {
+                    aggrLocations.add(new AFCAggrLocation(measurement.getMeasurement(), observation));
+                }
+            }
+            aggrLocations.sort();
+
+            List<AFCAggrLocation> list = aggrLocations.list;
+            if (list.size() > 1) {
+                Iterator<AFCAggrLocation> iterator = list.iterator();
+                AFCAggrLocation previous = iterator.next();
+                while (iterator.hasNext()) {
+                    AFCAggrLocation current = iterator.next();
+                    if (previous.equals(current)) {
+                        iterator.remove(); continue;
+                    }
+                    previous = current;
+                }
+            }
+
+            return aggrLocations;
+        }
+    }
+
+    private static class Converter {
+
+        private final static Logger logger = LogManager.getLogger(Converter.class);
+
+        private static final String DELIMITER = "&";
+
+        static String assemblyId(String... parts) {
+            return String.join(DELIMITER, parts);
+        }
+        
+        static String[] disassemblyId(String id) {
+            return id.split(DELIMITER);
+        }
+
+        static Filter convertFilter(io.connector.model.sensorthings.Filter ogcFilter) {
+            if (ogcFilter == null) {
+                throw new IllegalArgumentException("Unsupported filter attributes in filter expression '" + ogcFilter + "'.");
+            }
+
+            Filter afcFilter = new Filter();
+
+            for (io.connector.model.sensorthings.Filter.Expression expression : ogcFilter.getAddExpressions()) {
+                switch (expression.getAttribute()) {
+                    case "time":
+                    case "resultTime": {
+                        switch (expression.getOperator()) {
+                            case "lt": {
+                                afcFilter.endTime(Instant.parse(expression.getValue()));
+                            } break;
+                            case "gt": {
+                                afcFilter.startTime(Instant.parse(expression.getValue()));
+                            }break;
+                            default: throw new IllegalArgumentException(format(
+                                        "Unsupported operator '%s' in the filter expression '%s'.", expression.getOperator(), ogcFilter));
+                        }
+                    } break;
+                    default: throw new IllegalArgumentException(format(
+                                "Unsupported attribute '%s' in the filter expression '%s'.", expression.getAttribute(), ogcFilter));
+                }
+            }
+
+            return afcFilter;
+        }
+
+        static HistoricalLocation convertToHistoricalLocation(MultiSensor afcMultiSensor, AFCAggrLocation afcLocation, RequestUriComponent uriComponent) {
+            HistoricalLocation historicalLocation = new HistoricalLocation();
+            String locationId = assemblyId(afcMultiSensor.getResourceUrn(), afcLocation.measurement, afcLocation.observation.getTime());
+            String absoluteUrl = uriComponent.getGatewayUri();
+            historicalLocation.setId(locationId);
+            historicalLocation.setSelfLink(format("%s/HistoricalLocations(%s)", absoluteUrl, locationId));
+            historicalLocation.setLocationsNavigationLink(format("%s/HistoricalLocations(%s)/Locations", absoluteUrl, locationId));
+            historicalLocation.setThingNavigationLink(format("%s/HistoricalLocations(%s)/Thing", absoluteUrl, locationId));
+            historicalLocation.setTime(afcLocation.observation.getTime());
+            return historicalLocation;
+        }
+
+        static List<HistoricalLocation> convertToHistoricalLocation(MultiSensor afcMultiSensor, List<AFCAggrLocation> afcLocations, RequestUriComponent uriComponent) {
+            List<HistoricalLocation> historicalLocations = new ArrayList<>(afcLocations.size());
+            for (AFCAggrLocation afcLocation : afcLocations) {
+                historicalLocations.add(convertToHistoricalLocation(afcMultiSensor, afcLocation, uriComponent));
+            }
+            return historicalLocations;
+        }
+
+        static Location convertToLocation(MultiSensor afcMultiSensor, AFCAggrLocation afcLocation, RequestUriComponent uriComponent) {
+            Location location = new Location();
+            String locationId = assemblyId(afcMultiSensor.getResourceUrn(), afcLocation.measurement, afcLocation.observation.getTime());
+            String absoluteUrl = uriComponent.getGatewayUri();
+            location.setId(locationId);
+            location.setSelfLink(format("%s/Locations(%s)", absoluteUrl, locationId));
+            location.setHistoricalLocationsNavigationLink(format("%s/Locations(%s)/HistoricalLocations", absoluteUrl, locationId));
+            location.setName(afcMultiSensor.getResourceType());
+            location.setDescription(afcLocation.observation.getProvider());
+            location.setEncodingType("application/vnd.geo+json");
+
+            LocationInfo info = new LocationInfo();
+            info.setType("Feature");
+            location.setLocation(info);
+
+            Geometry geometry = new Geometry();
+            geometry.setType("Point");
+            io.connector.model.afarcloud.Observation afcObservation = afcLocation.observation;
+            geometry.setCoordinates(asList(afcObservation.getLongitude(), afcObservation.getLatitude(), afcObservation.getAltitude()));
+            info.setGeometry(geometry);
+
+            return location;
+        }
+
+        static Observation convertToObservation(ResourceMeasurement afcMeasurement, SensorTelemetry afcTelemetry, io.connector.model.afarcloud.Observation afcObservation, RequestUriComponent uriComponent) {
+            Observation observation = new Observation();
+            String observationId = assemblyId(afcMeasurement.getResource(), afcTelemetry.getMeasurement(), afcObservation.getTime());
+            String absoluteUrl = uriComponent.getGatewayUri();
+            observation.setId(observationId);
+            observation.setSelfLink(format("%s/Observations(%s)", absoluteUrl, observationId));
+            // observation.setFeatureOfInterestNavigationLink(format("Observations(%s)/FeatureOfInterest", observationId));
+            observation.setFeatureOfInterestNavigationLink("https://storage07-afarcloud.qa.pdmfc.com/storage/rest/registry/getAllObservationTypes");
+            observation.setDataStreamNavigationLink(format("%s/Observations(%s)/Datastream", absoluteUrl, observationId));
+            observation.setPhenomenonTime(afcObservation.getTime());
+            observation.setResultTime(afcObservation.getTime());
+            observation.setResult(afcObservation.getValue());
+            return observation;
+        }
+
+        static List<Observation> convertToObservations(ResourceMeasurement afcMeasurement, SensorTelemetry afcTelemetry, RequestUriComponent uriComponent) {
+            List<Observation> ogcObservations = new ArrayList<>(afcTelemetry.getObservations().size());
+            for (io.connector.model.afarcloud.Observation afcObservation : afcTelemetry.getObservations()) {
+                ogcObservations.add(convertToObservation(afcMeasurement, afcTelemetry, afcObservation, uriComponent));
+            }
+            return ogcObservations;
+        }
+
+        static ObservedProperty convertToObservedProperty(MultiSensor afcMultiSensor, MultiSensor.SensorSchema afcSensor, RequestUriComponent uriComponent) {
+            Objects.requireNonNull(afcMultiSensor);
+            Objects.requireNonNull(afcSensor);
+            Objects.requireNonNull(uriComponent);
+
+            ObservedProperty observedProperty = new ObservedProperty();
+            String observedPropertyId = assemblyId(afcMultiSensor.getResourceUrn(), afcSensor.getObservedProperty());
+            String absoluteUrl = uriComponent.getGatewayUri();
+            observedProperty.setId(observedPropertyId);
+            observedProperty.setSelfLink(format("%s/ObservedProperties(%s)", absoluteUrl, observedPropertyId));
+            observedProperty.setDataStreamNavigationLink(format("%s/ObservedProperties(%s)/Datastream", absoluteUrl, observedPropertyId));
+            observedProperty.setName(afcSensor.getObservedProperty());
+            observedProperty.setDescription(afcSensor.getObservedProperty());
+            observedProperty.setDefinition(afcSensor.getUom());
+            return observedProperty;
+        }
+
+        static Thing convertToThing(MultiSensor afcMultiSensor, RequestUriComponent uriComponent) {
+            Objects.requireNonNull(afcMultiSensor);
+            Objects.requireNonNull(uriComponent);
+
+            Thing thing = new Thing();
+            String thingId = afcMultiSensor.getResourceUrn();
+            String absoluteUrl = uriComponent.getGatewayUri();
+            thing.setId(thingId);
+            thing.setSelfLink(format("%s/Things(%s)", absoluteUrl, thingId));
+            thing.setLocationsNavigationLink(format("%s/Things(%s)/Locations", absoluteUrl, thingId));
+            thing.setDataStreamNavigationLink(format("%s/Things(%s)/Datastreams", absoluteUrl, thingId));
+            thing.setHistoricalLocationsNavigationLink(format("%s/Things(%s)/HistoricalLocations", absoluteUrl, thingId));
+            thing.setName(afcMultiSensor.getResourceType());
+            thing.setDescription(afcMultiSensor.getResourceType());
+            thing.setProperties(null);
+            return thing;
+        }
+
+        static List<Thing> convertToThing(List<MultiSensor> afcMultiSensors, RequestUriComponent uriComponent) {
+            List<Thing> ogcThings = new ArrayList<>(afcMultiSensors.size());
+            for (MultiSensor afcMultiSensor : afcMultiSensors) {
+                ogcThings.add(convertToThing(afcMultiSensor, uriComponent));
+            }
+            return ogcThings;
+        }
+
+        static Datastream convertToDataStream(MultiSensor afcMultiSensor, MultiSensor.SensorSchema afcSensor, SensorTelemetry telemetry, RequestUriComponent uriComponent) {
+            Objects.requireNonNull(afcMultiSensor);
+            Objects.requireNonNull(afcSensor);
+            Objects.requireNonNull(uriComponent);
+
+            Datastream datastream = new Datastream();
+            String dataStreamId = assemblyId(afcMultiSensor.getResourceUrn(), afcSensor.getObservedProperty());
+            String absoluteUrl = uriComponent.getGatewayUri();
+            datastream.setId(dataStreamId);
+            datastream.setSelfLink(format("%s/Datastreams(%s)", absoluteUrl, dataStreamId));
+            datastream.setThingNavigationLink(format("%s/Datastreams(%s)/Thing", absoluteUrl, dataStreamId));
+            datastream.setSensorNavigationLink(format("%s/Datastreams(%s)/Sensor", absoluteUrl, dataStreamId));
+            datastream.setObservedPropertyNavigationLink(format("%s/Datastreams(%s)/ObservedProperty", absoluteUrl, dataStreamId));
+            datastream.setObservationsNavigationLink(format("%s/Datastreams(%s)/Observations", absoluteUrl, dataStreamId));
+            datastream.setName(afcSensor.getObservedProperty());
+            datastream.setDescription(afcSensor.getObservedProperty());
+
+            UnitOfMeasurement uom = new UnitOfMeasurement();
+            uom.setName(afcSensor.getObservedProperty());
+            uom.setSymbol("");
+            uom.setDefinition(afcSensor.getUom());
+            datastream.setUnitOfMeasurement(uom);
+
+            datastream.setObservationType("http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement");
+
+            Geometry geometry = new Geometry();
+            geometry.setType("Point");
+            geometry.setCoordinates(asList(afcMultiSensor.getLongitude(), afcMultiSensor.getLatitude()));
+            datastream.setObservedArea(geometry);
+
+            String startDate = "<none>";
+            String endDate = telemetry == null || telemetry.getObservations().isEmpty() ? "<none>" : telemetry.getObservations().get(0).getTime();
+            String time = startDate + "/" + endDate;
+            datastream.setPhenomenonTime(time);
+            datastream.setResultTime(time);
+
+            return datastream;
+        }
+
+        static List<Datastream> convertToDataStream(MultiSensor afcMultiSensor, ResourceMeasurement measurement, RequestUriComponent uriComponent) {
+            Objects.requireNonNull(afcMultiSensor);
+            Objects.requireNonNull(uriComponent);
+
+            List<Datastream> datastreams = new ArrayList<>(afcMultiSensor.getObservations().size());
+
+            Map<String, SensorTelemetry> sensors = new HashMap<>();
+            if (measurement != null) {
+                for (SensorTelemetry telemetry : measurement.getMeasurements()) {
+                    sensors.put(telemetry.getMeasurement(), telemetry);
+                }
+            }
+
+            for (MultiSensor.SensorSchema sensor : afcMultiSensor.getObservations()) {
+                datastreams.add(convertToDataStream(afcMultiSensor, sensor, sensors.get(sensor.getObservedProperty()), uriComponent));
+            }
+
+            return datastreams;
+        }
+
+        static Sensor convertToSensor(MultiSensor afcMultiSensor, RequestUriComponent uriComponent) {
+            Objects.requireNonNull(afcMultiSensor);
+            Objects.requireNonNull(uriComponent);
+
+            String afcDomain = "https://storage07-afarcloud.qa.pdmfc.com/storage/rest";
+
+            Sensor sensor = new Sensor();
+            String thingId = afcMultiSensor.getResourceUrn();
+            String absoluteUrl = uriComponent.getGatewayUri();
+            sensor.setId(thingId);
+            sensor.setSelfLink(format("%s/Sensors(%s)", absoluteUrl, thingId));
+            sensor.setDataStreamNavigationLink(format("%s/Sensors(%s)/Datastreams", absoluteUrl, thingId));
+            sensor.setName(afcMultiSensor.getResourceType());
+            sensor.setDescription(afcMultiSensor.getResourceType());
+            sensor.setEncodingType("application/json");
+            sensor.setMetadata(format("%s/registry/getSensor/%s", afcDomain, afcMultiSensor.getResourceId()));
+
+            return sensor;
+        }
+
+        static MultiSimpleObservation convertToMultiSimpleObservation(ObservationInsert ogcObservation) {
+            String [] idPartsAfc = disassemblyId(ogcObservation.getDatastreamId());
+            String resourceUrn = idPartsAfc[0], observedProperty = idPartsAfc[1];
+            String [] urnPartsAfc = resourceUrn.split(":");
+            String resourceId = urnPartsAfc[urnPartsAfc.length-1];
+
+            MultiSimpleObservation multiSimpleObservation = new MultiSimpleObservation();
+            multiSimpleObservation.setResourceId(resourceId);
+
+            SimpleObservation simpleObservation = new SimpleObservation();
+            simpleObservation.setObservedProperty(observedProperty);
+            simpleObservation.setResultTime(ogcObservation.getPhenomenonTime().toEpochSecond());
+            simpleObservation.setResult(ogcObservation.getResult());
+
+            multiSimpleObservation.setObservations(singletonList(simpleObservation));
+
+            return multiSimpleObservation;
+        }
+
+        static MultiSensor convertToMultiSensor(ThingInsert ogcThing) {
+            String afcResourceType = ogcThing.getName();
+            if (!afcResourceType.equals(ogcThing.getDescription())) {
+                throw new IllegalArgumentException("Attribute 'description' has to be equal with 'name'. Contains 'resourceType' attribute from AFC.");
+            }
+            if (ogcThing.getLocations().size() != 1) {
+                throw new IllegalArgumentException("Attribute 'Locations' has to contain one location.");
+            }
+            Location ogcLocation = ogcThing.getLocations().get(0);
+            if (!afcResourceType.equals(ogcLocation.getName())) {
+                throw new IllegalArgumentException("Attribute 'Location/name' has to be equal with 'name'. Contains 'resourceType attribute from AFC.");
+            }
+            LocationInfo ogcLocationInfo = ogcLocation.getLocation();
+            String ogcLocationInfoType = ogcLocationInfo.getType();
+            if (!ogcLocationInfoType.equals("Feature")) {
+                throw new IllegalArgumentException("Allowed only 'Feature' type of location.");
+            }
+            Geometry ogcLocationGeometry = ogcLocationInfo.getGeometry();
+            if (!ogcLocationGeometry.getType().equals("Point")) {
+                throw new IllegalArgumentException("Allowed only 'Point' type of geometry.");
+            }
+            List<Double> ogcCoordinates = ogcLocationGeometry.getCoordinates();
+            if (ogcCoordinates.size() != 3) {
+                throw new IllegalArgumentException("Coordinates of location have to be in following format [longitude, latitude, altitude].");
+            }
+
+            double afcLongitude = ogcCoordinates.get(0);
+            double afcLatitude = ogcCoordinates.get(1);
+            double afcAltitude = ogcCoordinates.get(2);
+
+            List<DatastreamInsert> ogcDatastreams = ogcThing.getDatastreams();
+            List<MultiSensor.SensorSchema> afcSensorSchemas = new ArrayList<>(ogcDatastreams.size());
+            for (DatastreamInsert ogcDatastream : ogcDatastreams) {
+                String afcObservedProperty = ogcDatastream.getName();
+                if (!afcObservedProperty.equals(ogcDatastream.getDescription())) {
+                    throw new IllegalArgumentException("Attribute 'Datastream/description' has to be equal with 'Datastream/name'. Contains 'observedProperty attribute from AFC.");
+                }
+                UnitOfMeasurement ogcUnitOfMeasurement = ogcDatastream.getUnitOfMeasurement();
+                String afcUom = ogcUnitOfMeasurement.getDefinition();
+
+                String ogcObservationType = ogcDatastream.getObservationType();
+                if (!ogcObservationType.equals("http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement")) {
+                    throw new IllegalArgumentException("For the attribute 'Datastream/observationType' is allowed following values [http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement].");
+                }
+
+                Geometry ogcObservedArea = ogcDatastream.getObservedArea();
+                if (!ogcObservedArea.getType().equals("Point")) {
+                    throw new IllegalArgumentException("Allowed only 'Point' type of observedArea for the datastream '"+afcObservedProperty+"'.");
+                }
+                List<Double> ogcObcAreaCoordinates = ogcObservedArea.getCoordinates();
+                if (ogcObcAreaCoordinates.size() != 3) {
+                    throw new IllegalArgumentException("Coordinates of location for the datastream '"+afcObservedProperty+"' have to be in following format [longitude, latitude, altitude].");
+                }
+                if (!(ogcObcAreaCoordinates.get(0).equals(afcLongitude) || ogcObcAreaCoordinates.get(1).equals(afcLatitude) || ogcObcAreaCoordinates.get(2).equals(afcAltitude))) {
+                    throw new IllegalArgumentException("Coordinates of the observedArea have to be same as the last location.");
+                }
+
+                ObservedProperty ogcObservedProperty = ogcDatastream.getObservedProperty();
+                if (!ogcObservedProperty.getName().equals(afcObservedProperty)) {
+                    throw new IllegalArgumentException("Name of ObservedProperty has to be same as the name of datastream.");
+                }
+                if (!ogcObservedProperty.getDescription().equals(afcObservedProperty)) {
+                    throw new IllegalArgumentException("Description of ObservedProperty has to be same as the name of the datastream.");
+                }
+                if (!ogcObservedProperty.getDefinition().equals(afcUom)) {
+                    throw new IllegalArgumentException("Definition of ObservedProperty has to be same as the definition of unit measurement.");
+                }
+
+                SensorInsert ogcSensor = ogcDatastream.getSensor();
+                if (!ogcSensor.getName().equals(afcResourceType)) {
+                    throw new IllegalArgumentException("Name of sensor has to be same as the name of thing.");
+                }
+                if (!ogcSensor.getDescription().equals(afcResourceType)) {
+                    throw new IllegalArgumentException("Description of sensor has to be same as the name of thing.");
+                }
+                if (!ogcSensor.getEncodingType().equals("application/json")) {
+                    throw new IllegalArgumentException("For the attribute 'datastream/sensor/encodingType is allowed only 'application/json' type.");
+                }
+
+                MultiSensor.SensorSchema afcSensor = new MultiSensor.SensorSchema();
+                afcSensor.setObservedProperty(afcObservedProperty);
+                afcSensor.setUom(afcUom);
+                afcSensor.setMin_value(Double.MIN_VALUE);   // TODO set min_value
+                afcSensor.setMax_value(Double.MAX_VALUE);   // TODO set max_value
+                afcSensor.setAccuracy(0.1);                 // TODO set accuracy
+                afcSensorSchemas.add(afcSensor);
+            }
+
+            if (ogcThing.getProperties() == null) {
+                final String[] propAttrs = new String[]{"resourceUrn","resourceId", "supportedProtocol", "hardwareVersion", "softwareVersion", "firmwareVersion", "preprocessing", "pythonScript"};
+                throw new IllegalArgumentException("Attribute 'properties' is required and has to contain following attributes " + Arrays.toString(propAttrs) + ".");
+            }
+            Map<String, Object> ogcProperties = ogcThing.getProperties();
+
+            String afcResourceId = (String) ogcProperties.get("resourceId");
+            if (afcResourceId == null) {
+                throw new IllegalArgumentException("Attribute 'properties/resourceId' is required.");
+            }
+            String afcResourceUrn = (String) ogcProperties.get("resourceUrn");
+            if (afcResourceUrn == null) {
+                throw new IllegalArgumentException("Attribute 'properties/resourceUrn' is required.");
+            }
+            String afcSupportedProtocol = (String) ogcProperties.get("supportedProtocol");
+            if (afcSupportedProtocol == null) {
+                throw new IllegalArgumentException("Attribute 'properties/supportedProtocol' is required.");
+            }
+            String afcHardwareVersion = (String) ogcProperties.get("hardwareVersion");
+            if (afcHardwareVersion == null) {
+                throw new IllegalArgumentException("Attribute 'properties/hardwareVersion' is required.");
+            }
+            String afcSoftwareVersion = (String) ogcProperties.get("softwareVersion");
+            if (afcSoftwareVersion == null) {
+                throw new IllegalArgumentException("Attribute 'properties/softwareVersion' is required.");
+            }
+            String afcFirmwareVersion = (String) ogcProperties.get("firmwareVersion");
+            if (afcFirmwareVersion == null) {
+                throw new IllegalArgumentException("Attribute 'properties/firmwareVersion' is required.");
+            }
+            String afcPythonScript = (String) ogcProperties.get("pythonScript");
+            if (afcPythonScript == null) {
+                throw new IllegalArgumentException("Attribute 'properties/pythonScript' is required.");
+            }
+            Boolean afcPreprocessing = (Boolean) ogcProperties.get("preprocessing");
+            if (afcPreprocessing == null) {
+                throw new IllegalArgumentException("Attribute 'properties/preprocessing' is required.");
+            }
+
+            MultiSensor afcMultiSensor = new MultiSensor();
+            afcMultiSensor.setResourceId(afcResourceId);
+            afcMultiSensor.setResourceType(afcResourceType);
+            afcMultiSensor.setResourceUrn(afcResourceUrn);
+            afcMultiSensor.setLongitude(afcLongitude);
+            afcMultiSensor.setLatitude(afcLatitude);
+            afcMultiSensor.setAltitude(afcAltitude);
+            afcMultiSensor.setPreprocessing(afcPreprocessing);
+            afcMultiSensor.setPythonScript(afcPythonScript);
+            afcMultiSensor.setObservations(afcSensorSchemas);
+            afcMultiSensor.setSupportedProtocol(afcSupportedProtocol);
+            afcMultiSensor.setHardwareVersion(afcHardwareVersion);
+            afcMultiSensor.setSoftwareVersion(afcSoftwareVersion);
+            afcMultiSensor.setFirmwareVersion(afcFirmwareVersion);
+
+            return afcMultiSensor;
+        }
+    }
+}

+ 600 - 600
postman/SenslogV1.postman_collection.json

@@ -1,601 +1,601 @@
-{
-	"info": {
-		"_postman_id": "e00d63b3-2f1d-4d78-990d-975ffca54a8c",
-		"name": "SenslogV1",
-		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
-	},
-	"item": [
-		{
-			"name": "InsertObservation",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:9550/DBService/FeederServlet?Operation=InsertObservation&value=952.8&date=2015-07-15%2012%3A00%3A00%2B0200&unit_id=1&sensor_id=1",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "9550",
-					"path": [
-						"DBService",
-						"FeederServlet"
-					],
-					"query": [
-						{
-							"key": "Operation",
-							"value": "InsertObservation"
-						},
-						{
-							"key": "value",
-							"value": "952.8"
-						},
-						{
-							"key": "date",
-							"value": "2015-07-15%2012%3A00%3A00%2B0200"
-						},
-						{
-							"key": "unit_id",
-							"value": "1"
-						},
-						{
-							"key": "sensor_id",
-							"value": "1"
-						}
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "InsertUnitPosition",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:9550/DBService/FeederServlet?Operation=InsertPosition&lat=777.7&unit_id=1&lon=45.5&alt=40.0&speed=35.0&unit_id=1&date=2016-07-15%2012%3A00%3A00%2B0200",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "9550",
-					"path": [
-						"DBService",
-						"FeederServlet"
-					],
-					"query": [
-						{
-							"key": "Operation",
-							"value": "InsertPosition"
-						},
-						{
-							"key": "lat",
-							"value": "777.7"
-						},
-						{
-							"key": "unit_id",
-							"value": "1"
-						},
-						{
-							"key": "lon",
-							"value": "45.5"
-						},
-						{
-							"key": "alt",
-							"value": "40.0"
-						},
-						{
-							"key": "speed",
-							"value": "35.0"
-						},
-						{
-							"key": "unit_id",
-							"value": "1"
-						},
-						{
-							"key": "date",
-							"value": "2016-07-15%2012%3A00%3A00%2B0200"
-						}
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Things",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Things"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Things(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Things(1)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Datastreams(:id)/Thing",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/Thing",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Datastreams(1:1)",
-						"Thing"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "HistoricalLocations(:id)/Thing",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/HistoricalLocations(1)/Thing",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"HistoricalLocations(1)",
-						"Thing"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Datastreams(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Datastreams(1:1)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Things(:id)/Datastreams",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)/Datastreams",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Things(1)",
-						"Datastreams"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Sensors(:id)/Datastreams",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Sensors(1)/Datastreams",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Sensors(1)",
-						"Datastreams"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "ObservedProperties(:id)/Datastream",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/ObservedProperties(999999)/Datastream",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"ObservedProperties(999999)",
-						"Datastream"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Observations(:id)/Datastream",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Observations(1)/Datastream",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Observations(1)",
-						"Datastream"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Sensors(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Sensors(1)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Sensors(1)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Datastreams(:id)/Sensor",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/Sensor",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Datastreams(1:1)",
-						"Sensor"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "ObservedProperties(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/ObservedProperties(999999)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"ObservedProperties(999999)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Datastreams(:id)/ObservedProperty",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/ObservedProperty",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Datastreams(1:1)",
-						"ObservedProperty"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Observations(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Observations(1)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Observations(1)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Datastreams(:id)/Observations",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/Observations",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Datastreams(1:1)",
-						"Observations"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Locations(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Locations(1)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Locations(1)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Things(:id)/Locations",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)/Locations",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Things(1)",
-						"Locations"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "HistoricalLocation(id)/Location",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/HistoricalLocations(1)/Locations",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"HistoricalLocations(1)",
-						"Locations"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "HistoricalLocations(:id)",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/HistoricalLocations(1)",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"HistoricalLocations(1)"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Things(id)/HistoricalLocation",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)/HistoricalLocations",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Things(1)",
-						"HistoricalLocations"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Locations(:id)/HistoricalLocations",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Locations(1)/HistoricalLocations",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"SensLogV1",
-						"OGCSensorThings",
-						"Locations(1)",
-						"HistoricalLocations"
-					]
-				}
-			},
-			"response": []
-		},
-		{
-			"name": "Info",
-			"request": {
-				"method": "GET",
-				"header": [],
-				"url": {
-					"raw": "http://localhost:8080/api/info",
-					"protocol": "http",
-					"host": [
-						"localhost"
-					],
-					"port": "8080",
-					"path": [
-						"api",
-						"info"
-					]
-				}
-			},
-			"response": []
-		}
-	],
-	"protocolProfileBehavior": {}
+{
+	"info": {
+		"_postman_id": "e00d63b3-2f1d-4d78-990d-975ffca54a8c",
+		"name": "SenslogV1",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+	},
+	"item": [
+		{
+			"name": "InsertObservation",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:9550/DBService/FeederServlet?Operation=InsertObservation&value=952.8&date=2015-07-15%2012%3A00%3A00%2B0200&unit_id=1&sensor_id=1",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "9550",
+					"path": [
+						"DBService",
+						"FeederServlet"
+					],
+					"query": [
+						{
+							"key": "Operation",
+							"value": "InsertObservation"
+						},
+						{
+							"key": "value",
+							"value": "952.8"
+						},
+						{
+							"key": "date",
+							"value": "2015-07-15%2012%3A00%3A00%2B0200"
+						},
+						{
+							"key": "unit_id",
+							"value": "1"
+						},
+						{
+							"key": "sensor_id",
+							"value": "1"
+						}
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "InsertUnitPosition",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:9550/DBService/FeederServlet?Operation=InsertPosition&lat=777.7&unit_id=1&lon=45.5&alt=40.0&speed=35.0&unit_id=1&date=2016-07-15%2012%3A00%3A00%2B0200",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "9550",
+					"path": [
+						"DBService",
+						"FeederServlet"
+					],
+					"query": [
+						{
+							"key": "Operation",
+							"value": "InsertPosition"
+						},
+						{
+							"key": "lat",
+							"value": "777.7"
+						},
+						{
+							"key": "unit_id",
+							"value": "1"
+						},
+						{
+							"key": "lon",
+							"value": "45.5"
+						},
+						{
+							"key": "alt",
+							"value": "40.0"
+						},
+						{
+							"key": "speed",
+							"value": "35.0"
+						},
+						{
+							"key": "unit_id",
+							"value": "1"
+						},
+						{
+							"key": "date",
+							"value": "2016-07-15%2012%3A00%3A00%2B0200"
+						}
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Things",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Things"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Things(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Things(1)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Datastreams(:id)/Thing",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/Thing",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Datastreams(1:1)",
+						"Thing"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "HistoricalLocations(:id)/Thing",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/HistoricalLocations(1)/Thing",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"HistoricalLocations(1)",
+						"Thing"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Datastreams(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Datastreams(1:1)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Things(:id)/Datastreams",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)/Datastreams",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Things(1)",
+						"Datastreams"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Sensors(:id)/Datastreams",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Sensors(1)/Datastreams",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Sensors(1)",
+						"Datastreams"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "ObservedProperties(:id)/Datastream",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/ObservedProperties(999999)/Datastream",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"ObservedProperties(999999)",
+						"Datastream"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Observations(:id)/Datastream",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Observations(1)/Datastream",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Observations(1)",
+						"Datastream"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Sensors(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Sensors(1)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Sensors(1)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Datastreams(:id)/Sensor",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/Sensor",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Datastreams(1:1)",
+						"Sensor"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "ObservedProperties(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/ObservedProperties(999999)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"ObservedProperties(999999)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Datastreams(:id)/ObservedProperty",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/ObservedProperty",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Datastreams(1:1)",
+						"ObservedProperty"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Observations(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Observations(1)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Observations(1)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Datastreams(:id)/Observations",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Datastreams(1:1)/Observations",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Datastreams(1:1)",
+						"Observations"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Locations(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Locations(1)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Locations(1)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Things(:id)/Locations",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)/Locations",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Things(1)",
+						"Locations"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "HistoricalLocation(id)/Location",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/HistoricalLocations(1)/Locations",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"HistoricalLocations(1)",
+						"Locations"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "HistoricalLocations(:id)",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/HistoricalLocations(1)",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"HistoricalLocations(1)"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Things(id)/HistoricalLocation",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Things(1)/HistoricalLocations",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Things(1)",
+						"HistoricalLocations"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Locations(:id)/HistoricalLocations",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/SensLogV1/OGCSensorThings/Locations(1)/HistoricalLocations",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"SensLogV1",
+						"OGCSensorThings",
+						"Locations(1)",
+						"HistoricalLocations"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Info",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "http://localhost:8080/api/info",
+					"protocol": "http",
+					"host": [
+						"localhost"
+					],
+					"port": "8080",
+					"path": [
+						"api",
+						"info"
+					]
+				}
+			},
+			"response": []
+		}
+	],
+	"protocolProfileBehavior": {}
 }

+ 4 - 4
settings.gradle

@@ -1,5 +1,5 @@
-rootProject.name = 'connector'
-include 'connector-app', 'connector-core', 'connector-model'
-include 'connector-module-afarcloud'
-include 'connector-module-senslog1'
+rootProject.name = 'connector'
+include 'connector-app', 'connector-core', 'connector-model'
+include 'connector-module-afarcloud'
+include 'connector-module-senslog1'
 include 'connector-module-ima'

+ 10 - 10
src/main/java/io/connector/Main.java

@@ -1,10 +1,10 @@
-package io.connector;
-
-import io.connector.app.Application;
-
-public class Main {
-
-    public static void main(String[] args) throws Exception {
-        Application.init(args).start();
-    }
-}
+package io.connector;
+
+import io.connector.app.Application;
+
+public class Main {
+
+    public static void main(String[] args) throws Exception {
+        Application.init(args).start();
+    }
+}