bug6059 aftermath: added DAW filter for "last message/fix" query

This commit is contained in:
Axel Uhl committed 2025-01-09 14:59:20 +01:00
1 parent d6156144b0
commit 36442e93f3
8 files changed
+142 -43

No files matched your search

+1
View File
@@ -24,6 +24,7 @@ SAP is at the center of today’s technology revolution, developing innovations
* [[Runtime Environment|wiki/info/landscape/runtime-environment]]
* [[Basic architectual principles|wiki/info/landscape/basic-architectural-principles]]
* [[User Management|wiki/info/landscape/usermanagement]]
* [[Igtimi Windbot Riot Connector|wiki/info/landscape/igtimi-riot]]
* [[Development Environment|wiki/info/landscape/development-environment]]
* [[Production Environment|wiki/info/landscape/production-environment]]
* [[Internationalization (i18n)|wiki/howto/development/i18n]]
@@ -6,6 +6,7 @@ import java.util.Base64.Encoder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.ws.rs.GET;
@@ -31,6 +32,7 @@ import com.sap.sailing.domain.igtimiadapter.Device;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnection;
import com.sap.sailing.domain.igtimiadapter.datatypes.Type;
import com.sap.sailing.domain.igtimiadapter.server.riot.RiotServer;
import com.sap.sse.common.MultiTimeRange;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
@@ -57,11 +59,14 @@ public class RiotResourcesResource extends AbstractRiotServerResource {
final Iterable<String> serialNumbersToUse = serialNumbers==null || serialNumbers.isEmpty()
? Util.map(getRiotService().getDevices(), d->d.getSerialNumber())
: serialNumbers;
final Iterable<DataAccessWindow> daws = getDataAccessWindowsReadableBySubject(MultiTimeRange.of(TimeRange.create(null, null)), serialNumbersToUse);
final Map<String, MultiTimeRange> multiTimeRangesPerDeviceSerialNumber = getTimeRangesByDeviceSerialNumber(daws);
for (final String serialNumber : serialNumbersToUse) {
final Device device = getRiotService().getDeviceBySerialNumber(serialNumber);
if (device != null && subject.isPermitted(
device.getIdentifier().getStringPermission(DefaultActions.READ))) {
final Msg lastMessage = riot.getLastMessage(serialNumber, DataCase.forNumber(type));
final Msg lastMessage = riot.getLastMessage(serialNumber, DataCase.forNumber(type), multiTimeRangesPerDeviceSerialNumber.get(serialNumber));
if (lastMessage != null) {
final JSONArray singleMessage = new JSONArray();
singleMessage.add(Base64.getEncoder().encodeToString(lastMessage.toByteArray()));
@@ -112,24 +117,38 @@ public class RiotResourcesResource extends AbstractRiotServerResource {
final Iterable<String> serialNumbersOfDevicesUserCanRead =
Util.filter(serialNumbers, serialNumber->subject.isPermitted(
getRiotService().getDeviceBySerialNumber(serialNumber).getIdentifier().getStringPermission(DefaultActions.READ)));
final Iterable<DataAccessWindow> daws = getDataAccessWindowsReadableBySubject(requestedTimeRange, serialNumbersOfDevicesUserCanRead);
final Iterable<DataAccessWindow> daws = getDataAccessWindowsReadableBySubject(MultiTimeRange.of(requestedTimeRange), serialNumbersOfDevicesUserCanRead);
final Map<String, MultiTimeRange> multiTimeRangesPerDeviceSerialNumber = getTimeRangesByDeviceSerialNumber(daws);
// assemble results from DAWs readable by the user:
final JSONObject result = new JSONObject();
final Encoder base64Encoder = Base64.getEncoder();
for (final DataAccessWindow daw : daws) {
final TimeRange timeRangeIntersectionBetweenRequestAndDAW = requestedTimeRange.intersection(daw.getTimeRange());
final Iterable<Msg> messagesForDAW = getRiotService().getMessages(daw.getDeviceSerialNumber(), timeRangeIntersectionBetweenRequestAndDAW, dataCases);
final JSONArray arrayForDevice = (JSONArray) result.computeIfAbsent(daw.getDeviceSerialNumber(), d->new JSONArray());
for (final Msg message : messagesForDAW) {
arrayForDevice.add(new String(base64Encoder.encode(message.toByteArray())));
for (final Entry<String, MultiTimeRange> deviceSerialNumberAndMultiTimeRange : multiTimeRangesPerDeviceSerialNumber.entrySet()) {
final MultiTimeRange timeRangeIntersectionBetweenRequestAndDAWs = deviceSerialNumberAndMultiTimeRange.getValue().intersection(requestedTimeRange);
if (!timeRangeIntersectionBetweenRequestAndDAWs.isEmpty()) {
final Iterable<Msg> messagesForDAW = getRiotService().getMessages(deviceSerialNumberAndMultiTimeRange.getKey(), timeRangeIntersectionBetweenRequestAndDAWs, dataCases);
final JSONArray arrayForDevice = (JSONArray) result.computeIfAbsent(deviceSerialNumberAndMultiTimeRange.getKey(), d->new JSONArray());
for (final Msg message : messagesForDAW) {
arrayForDevice.add(new String(base64Encoder.encode(message.toByteArray())));
}
}
}
return Response.ok(streamingOutput(result)).build();
}
private Iterable<DataAccessWindow> getDataAccessWindowsReadableBySubject(TimeRange timeRange, Iterable<String> serialNumbers) {
private Map<String, MultiTimeRange> getTimeRangesByDeviceSerialNumber(Iterable<DataAccessWindow> daws) {
final Map<String, MultiTimeRange> result = new HashMap<>();
for (final DataAccessWindow daw : daws) {
result.compute(daw.getDeviceSerialNumber(),
(serialNumber, multiTimeRange)->multiTimeRange == null
? MultiTimeRange.of(daw.getTimeRange())
: multiTimeRange.union(daw.getTimeRange()));
}
return result;
}
private Iterable<DataAccessWindow> getDataAccessWindowsReadableBySubject(MultiTimeRange timeRanges, Iterable<String> serialNumbers) {
final Subject subject = SecurityUtils.getSubject();
return Util.filter(getRiotService().getDataAccessWindows(serialNumbers, timeRange),
return Util.filter(getRiotService().getDataAccessWindows(serialNumbers, timeRanges),
daw->{
return subject.isPermitted(daw.getIdentifier().getStringPermission(DefaultActions.READ));
});
@@ -9,18 +9,18 @@ import com.igtimi.IgtimiStream.Msg;
import com.mongodb.client.ClientSession;
import com.sap.sailing.domain.igtimiadapter.DataAccessWindow;
import com.sap.sailing.domain.igtimiadapter.Device;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.MultiTimeRange;
public interface DomainObjectFactory {
Iterable<DataAccessWindow> getDataAccessWindows(ClientSession clientSessionOrNull);
Iterable<Device> getDevices(ClientSession clientSessionOrNull);
Iterable<Msg> getMessages(String deviceSerialNumber, TimeRange timeRange, Set<DataCase> dataCases, ClientSession clientSessionOrNull);
Iterable<Msg> getMessages(String deviceSerialNumber, MultiTimeRange timeRanges, Set<DataCase> dataCases, ClientSession clientSessionOrNull);
/**
* Finds the latest message received from the device identified by {@code deviceSerialNumber}
* that has a {@link DataPoint} of the correct {@link DataCase}.
*/
Msg getLatestMessage(String deviceSerialNumber, DataCase dataCase, ClientSession clientSessionOrNull) throws InvalidProtocolBufferException;
Msg getLatestMessage(String deviceSerialNumber, DataCase dataCase, MultiTimeRange timeRanges, ClientSession clientSessionOrNull) throws InvalidProtocolBufferException;
}
@@ -9,6 +9,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Binary;
import com.google.protobuf.InvalidProtocolBufferException;
@@ -26,6 +27,7 @@ import com.mongodb.client.model.Sorts;
import com.sap.sailing.domain.igtimiadapter.DataAccessWindow;
import com.sap.sailing.domain.igtimiadapter.Device;
import com.sap.sailing.domain.igtimiadapter.persistence.DomainObjectFactory;
import com.sap.sse.common.MultiTimeRange;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
@@ -79,9 +81,9 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
@Override
public Iterable<Msg> getMessages(String deviceSerialNumber, TimeRange timeRange, Set<DataCase> dataCases, ClientSession clientSessionOrNull) {
public Iterable<Msg> getMessages(String deviceSerialNumber, MultiTimeRange timeRanges, Set<DataCase> dataCases, ClientSession clientSessionOrNull) {
final Document query = new Document();
appendTimeRangeQuery(query, timeRange);
appendMultiTimeRangeQuery(query, timeRanges);
query.append(FieldNames.IGTIMI_MESSAGES_DEVICE_SERIAL_NUMBER.name(), deviceSerialNumber);
final Iterable<Document> queryResult = clientSessionOrNull == null ? messagesCollection.find(query)
: messagesCollection.find(clientSessionOrNull, query);
@@ -126,9 +128,10 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
@Override
public Msg getLatestMessage(String deviceSerialNumber, DataCase dataCase, ClientSession clientSessionOrNull) throws InvalidProtocolBufferException {
public Msg getLatestMessage(String deviceSerialNumber, DataCase dataCase, MultiTimeRange timeRanges, ClientSession clientSessionOrNull) throws InvalidProtocolBufferException {
final Document query = new Document();
query.append(FieldNames.IGTIMI_MESSAGES_DEVICE_SERIAL_NUMBER.name(), deviceSerialNumber);
appendMultiTimeRangeQuery(query, timeRanges);
final FindIterable<Document> documentsUnsorted = clientSessionOrNull == null ? messagesCollection.find(query)
: messagesCollection.find(clientSessionOrNull, query);
final Iterable<Document> queryResult = documentsUnsorted.sort(Sorts.descending(FieldNames.IGTIMI_MESSAGES_TIMESTAMP.name()));
@@ -160,16 +163,22 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
return null;
}
private void appendTimeRangeQuery(Document query, TimeRange timeRange) {
final Document timeRangeQuery = new Document();
if (timeRange.from() != null) {
timeRangeQuery.append("$gte", timeRange.from().asDate());
}
if (timeRange.to() != null) {
timeRangeQuery.append("$lt", timeRange.to().asDate());
}
if (!timeRangeQuery.isEmpty()) {
query.append(FieldNames.IGTIMI_MESSAGES_TIMESTAMP.name(), timeRangeQuery);
private void appendMultiTimeRangeQuery(Document query, MultiTimeRange timeRanges) {
final List<Bson> or = new ArrayList<>();
for (final TimeRange timeRange : timeRanges) {
final Document timeRangeQuery = new Document();
if (timeRange.from() != null) {
timeRangeQuery.append("$gte", timeRange.from().asDate());
}
if (timeRange.to() != null) {
timeRangeQuery.append("$lt", timeRange.to().asDate());
}
if (!timeRangeQuery.isEmpty()) {
final Document clauseForOr = new Document();
clauseForOr.append(FieldNames.IGTIMI_MESSAGES_TIMESTAMP.name(), timeRangeQuery);
or.add(clauseForOr);
}
}
query.append("$or", or);
}
}
@@ -28,8 +28,8 @@ import com.sap.sailing.domain.igtimiadapter.server.replication.ReplicableRiotSer
import com.sap.sailing.domain.igtimiadapter.server.replication.RiotReplicationOperation;
import com.sap.sailing.domain.igtimiadapter.server.riot.impl.RiotServerImpl;
import com.sap.sailing.domain.igtimiadapter.shared.IgtimiWindReceiver;
import com.sap.sse.common.MultiTimeRange;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
import com.sap.sse.replication.Replicable;
@@ -112,7 +112,7 @@ public interface RiotServer extends Replicable<ReplicableRiotServer, RiotReplica
/**
* @param serialNumbers must not be {@link null}; if empty, no results will be delivered
*/
Iterable<DataAccessWindow> getDataAccessWindows(Iterable<String> serialNumbers, TimeRange timeRange);
Iterable<DataAccessWindow> getDataAccessWindows(Iterable<String> serialNumbers, MultiTimeRange timeRanges);
DataAccessWindow createDataAccessWindow(String deviceSerialNumber, TimePoint startTime, TimePoint endTime);
@@ -123,7 +123,8 @@ public interface RiotServer extends Replicable<ReplicableRiotServer, RiotReplica
* {@link IgtimiConnection} is established to the REST API end point of the primary/master instance of this replica
* to obtain the content requested.
*/
Iterable<Msg> getMessages(String deviceSerialNumber, TimeRange timeRange, Set<DataCase> dataCases) throws MalformedURLException, IllegalStateException, ClientProtocolException, IOException, ParseException;
Iterable<Msg> getMessages(String deviceSerialNumber, MultiTimeRange timeRanges, Set<DataCase> dataCases)
throws MalformedURLException, IllegalStateException, ClientProtocolException, IOException, ParseException;
void addWebSocketClient(RiotWebsocketHandler riotWebsocketHandler);
@@ -138,10 +139,10 @@ public interface RiotServer extends Replicable<ReplicableRiotServer, RiotReplica
* a {@link DataPoint} with {@link DataPoint#getDataCase() data case} {@code dataCase}; otherwise that last
* message stripped down to the last data point with the requested data case.
*/
Msg getLastMessage(String serialNumber, DataCase dataCase) throws InvalidProtocolBufferException, ParseException, IOException;
Msg getLastMessage(String serialNumber, DataCase dataCase, MultiTimeRange timeRanges) throws InvalidProtocolBufferException, ParseException, IOException;
default <T extends Fix> T getLastFix(String serialNumber, Class<T> type) throws InvalidProtocolBufferException, ParseException, IOException {
final Msg lastMessage = getLastMessage(serialNumber, DataCase.forNumber(Type.getType(type).getCode()));
default <T extends Fix> T getLastFix(String serialNumber, Class<T> type, MultiTimeRange timeRanges) throws InvalidProtocolBufferException, ParseException, IOException {
final Msg lastMessage = getLastMessage(serialNumber, DataCase.forNumber(Type.getType(type).getCode()), timeRanges);
final T result;
if (lastMessage != null) {
final Iterable<Fix> fixes = new FixFactory().createFixes(lastMessage);
@@ -52,6 +52,7 @@ import com.sap.sailing.domain.igtimiadapter.server.riot.RiotMessageListener;
import com.sap.sailing.domain.igtimiadapter.server.riot.RiotServer;
import com.sap.sailing.domain.igtimiadapter.server.riot.RiotStandardCommand;
import com.sap.sse.common.Duration;
import com.sap.sse.common.MultiTimeRange;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
@@ -277,7 +278,7 @@ public class RiotServerImpl extends AbstractReplicableWithObjectInputStream<Repl
final byte[] messageAsBytes = message.toByteArray();
final SecurityService securityService = getSecurityService();
final TimeRange messageDataTimeRange = getTimeRange(message);
final Iterable<DataAccessWindow> daws = getDataAccessWindows(Collections.singleton(deviceSerialNumber), messageDataTimeRange);
final Iterable<DataAccessWindow> daws = getDataAccessWindows(Collections.singleton(deviceSerialNumber), MultiTimeRange.of(messageDataTimeRange));
for (final RiotWebsocketHandler webSocketClient : liveWebSocketConnections) {
final User user = webSocketClient.getAuthenticatedUser();
final OwnershipAnnotation deviceOwnership = securityService.getOwnership(device.getIdentifier());
@@ -413,12 +414,12 @@ public class RiotServerImpl extends AbstractReplicableWithObjectInputStream<Repl
}
@Override
public Iterable<DataAccessWindow> getDataAccessWindows(Iterable<String> deviceSerialNumbers, TimeRange timeRange) {
public Iterable<DataAccessWindow> getDataAccessWindows(Iterable<String> deviceSerialNumbers, MultiTimeRange timeRanges) {
final Set<DataAccessWindow> result = new HashSet<>();
final Set<String> deviceSerialNumbersAsSet = Util.asSet(deviceSerialNumbers);
// TODO provide a more efficient implementation if this turns out to become a performance bottleneck, e.g., by keeping the DataAccessWindows in a time-sorted TreeSet
for (final DataAccessWindow daw : getDataAccessWindows()) {
if (deviceSerialNumbersAsSet.contains(daw.getDeviceSerialNumber()) && timeRange.intersects(daw.getTimeRange())) {
if (deviceSerialNumbersAsSet.contains(daw.getDeviceSerialNumber()) && timeRanges.intersects(daw.getTimeRange())) {
result.add(daw);
}
}
@@ -494,18 +495,23 @@ public class RiotServerImpl extends AbstractReplicableWithObjectInputStream<Repl
}
@Override
public Iterable<Msg> getMessages(String deviceSerialNumber, TimeRange timeRange, Set<DataCase> dataCases) throws IllegalStateException, ClientProtocolException, IOException, ParseException {
public Iterable<Msg> getMessages(String deviceSerialNumber, MultiTimeRange timeRanges, Set<DataCase> dataCases) throws IllegalStateException, ClientProtocolException, IOException, ParseException {
final Iterable<Msg> result;
if (getMasterDescriptor() == null) {
result = domainObjectFactory.getMessages(deviceSerialNumber, timeRange, dataCases, /* clientSessionOrNull */ null);
result = domainObjectFactory.getMessages(deviceSerialNumber, timeRanges, dataCases, /* clientSessionOrNull */ null);
} else {
final Type[] types = new Type[dataCases.size()];
int i=0;
for (final DataCase dataCase : dataCases) {
types[i++] = Type.valueOf(dataCase.getNumber());
}
result = getFromPrimary(deviceSerialNumber, types,
(c, dsn, ts)->c.getMessages(timeRange.from(), timeRange.to(), Collections.singleton(dsn), ts));
result = Util.concat(Util.map(timeRanges, timeRange->{
try {
return getFromPrimary(deviceSerialNumber, types, (c, dsn, ts)->c.getMessages(timeRange.from(), timeRange.to(), Collections.singleton(dsn), ts));
} catch (ParseException | IOException e) {
throw new RuntimeException(e);
}
}));
}
return result;
}
@@ -542,10 +548,10 @@ public class RiotServerImpl extends AbstractReplicableWithObjectInputStream<Repl
}
@Override
public Msg getLastMessage(String serialNumber, DataCase dataCase) throws ParseException, IOException {
public Msg getLastMessage(String serialNumber, DataCase dataCase, MultiTimeRange timeRanges) throws ParseException, IOException {
final Msg result;
if (getMasterDescriptor() == null) {
result = domainObjectFactory.getLatestMessage(serialNumber, dataCase, /* clientSessionOrNull */ null);
result = domainObjectFactory.getLatestMessage(serialNumber, dataCase, timeRanges, /* clientSessionOrNull */ null);
} else {
result = getFromPrimary(serialNumber, new Type[] { Type.valueOf(dataCase.getNumber()) },
(c, dsn, ts)->c.getLastMessage(dsn, ts[0]));
@@ -4270,8 +4270,8 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet
device,
lastHeartBeatsByDeviceSerialNumber.get(device.getSerialNumber()),
remoteAddressByDeviceSerialNumber.get(device.getSerialNumber()),
getPositionFromMessage(riotServer.getLastFix(device.getSerialNumber(), GpsLatLong.class)),
getBatteryPercentFromMessage(riotServer.getLastFix(device.getSerialNumber(), BatteryLevel.class)));
getPositionFromMessage(riotServer.getLastFix(device.getSerialNumber(), GpsLatLong.class, timeRanges)),
getBatteryPercentFromMessage(riotServer.getLastFix(device.getSerialNumber(), BatteryLevel.class, timeRanges)));
} catch (org.json.simple.parser.ParseException | IOException e) {
throw new RuntimeException(e);
}
+63
View File
@@ -0,0 +1,63 @@
# Igtimi WindBot Riot Connector and Server
[[_TOC_]]
## Architecture Overview
Igtimi used to be a company based in Dunedin, New Zealand, makers of the YachtBot and WindBot devices for boat tracking and wind and current sensing. The devices come equipped with a "Brain" that holds a battery, a SIM card-based Internet connection, an SDCard for configuration and log file storage, and an IMU (Inertial Motion Unit) with a satellite positioning sensor and accelerometers. The WindBot comes with a Gill ultrasonic wind sensor at the top of a carbon fibre pole, connected to the brain through a cable running inside the hollow pole. When switched on, the devices will try to establish a TCP connection to a server (the "Riot" server), performing a handshake during which the device authenticates to the server using a "device group token," then sending sensor and status data.
The transmission protocol is based on Protocol Buffers (protobuf), a serialization / de-serialization standard developed by Google. It works both ways, so the server can also send protobuf messages to the devices through the socket connection, once established. This can be used for acknowledgement messages but also for sending commands to the device, e.g., in order to power it down. The protocol used by the WindBot devices has been documented [here](https://support.yacht-bot.com/YachtBot%20Products/Riot%20Protocol/). The technical specification of the protobuf message formats can be found in [this Github project](https://github.com/igtimi/go-igtimi/tree/main/proto).
The default address to which the devices connect is ``www.igtimi.com:6000``. Since [firmware version 2.2.540](https://support.yacht-bot.com/YachtBot%20Products/Firmware%20Updates/#firmware-update-process) it is possible to [configure](https://github.com/igtimi/go-igtimi?tab=readme-ov-file#device-configuration) the address to which the devices connect in their ``config.ini`` configuration file on their SDCard. Using a line like
```
riot server_address sapsailing.com 6000
```
the connection will be made to ``sapsailing.com:6000`` instead of the ``igtimi.com`` address.
Igtimi called the back-end to which the devices connect the "Riot" system. It has a REST API through which authenticated clients can discover devices, resources created from data sent from those devices, and data access windows which grant access to data recorded by a specific device for a specified time range to a specific set of users. The API then also allows clients to request the actual data that has been recorded so far, as well as connect with a web socket connection through which the server will stream data from requested devices live if the client authenticated with the web socket connection is permitted to read this data on the basis of one or more data access windows granting permission.
The device-to-Riot connections as well as the web socket connections from API clients to the Riot server implement a heart beat mechanism to notice broken connections and issue a re-connect.
## History of the Riot System at Igtimi and Riedel
Around 2020, the German company *Riedel Communications GmbH* acquired Igtimi and used some of their technology for the America's Cup at the time. The Riot system continued to operate under the Igtimi brand, though owned by Riedel now. In the years that followed, Riedel decided to move out of the sailing domain again. In 2024 they announced they would end the Riot service by the end of the year. Effective January 6, 2025, the Riot system running at ``https://www.yacht-bot.com`` and ``https://www.igtimi.com`` was shut down.
With the new firmware version 2.2.540, the remaining devices can now be made send to a new address that needs to receive the protobuf messages, handle the connection handshake and implement the heart beat mechanism accordingly. An example implementation was provided by Riedel as an [open-source project](https://github.com/igtimi/go-igtimi). With this, existing customers are now on their own when it comes to providing a new back-end for the devices still in use.
We decided to implement a Riot server replacement that comes as a few OSGi bundles which can be embedded in our existing Sailing Analytics architecture.
## Embedded Riot Server in the Sailing Analytics
We captured the requirement to build our own Riot server replacement in [bug 6059](https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=6059) and dependent tasks.
### OSGi Bundles
The solution is built around the existing Igtimi connector, mainly implemented by bundle ``com.sap.sailing.domain.igtimiadapter`` and web bundle ``com.sap.sailing.domain.igtimiadapter.gateway`` serving an API under the ``/igtimi`` URL path. The former had a client for the Riot REST API to authenticate with a client ID and a client secret, then query devices and data access windows, load stored data and connect to live data using web socket connections. The gateway bundle was used primarily as a callback interface to receive an OAuth token during the account authentication phase which was then used to authenticate API requests. A third bundle, ``com.sap.sailing.domain.igtimiadapter.persistence``, was used to store the account and token information persistently across server restarts, and a test fragment ``com.sap.sailing.domain.igtimiadapter.test`` ran a few tests for the API client.
A new bundle ``com.sap.sailing.domain.igtimiadapter.server`` was added that now implements the core Riot functionality, in particular the socket server that listens for incoming device connections. The persistence bundle was changed and now stores what the Riot server implementation needs: ``Device`` and ``DataAccessWindow`` entities as well as the messages received form the devices. This makes for three MongoDB collections managed by the bundle:
- ``IGTIMI_DATA_ACCESS_WINDOWS``
- ``IGTIMI_DEVICES``
- ``IGTIMI_MESSAGES``
The server bundle's activator loads the existing devices and data access windows from the database and keeps them in memory during its life span. It launches the socket server, using ``java.nio`` selectors for good resource utilization. Devices connecting and sending an authentication request will be sent an acknowledgement. If a device yet unknown connects for the first time, a new ``Device`` entity will be created automatically, although without any data access window. Messages received from any device will be stored in the database and will be forwarded to any live web socket client that registered for messages from that device and can ``READ`` a data access window for that device spanning the event's time point.
The gateway bundle offers API methods to create, query and update the device and data access window entities, as well as for downloading sensor data and connecting with web sockets for live sensor data. These API methods are not 100% compatible with the original Riot API but have been designed sufficiently similar, achieving the goal of reducing the need for change in the existing client connector implemented by the ``com.sap.sailing.domain.igtimiadapter`` bundle.
The ``com.sap.sailing.gwt.ui`` bundle, in particular its AdminConsole entry point and in it the ``IgtimiDevicesPanel`` as well as the corresponding ``SailingService`` GWT RPC methods have been adjusted. Instead of managing the old accounts required for accessing the Igtimi-hosted Riot API it now provides a view onto the Riot server running embedded in the Sailing Analytics server that the AdminConsole holding the Igtimi panel configures. Consequently, it shows the devices and their associated data access windows and offers basic support for sending commands to the devices as well as seeing their last known heart beat time point and last known position.
### Security Aspects
Devices and data access windows are secured entities that support the usual group and user ownerships as well as access control lists and the default actions ``CREATE``, ``READ``, ``UPDATE``, and ``DELETE``. As usual, the GWT UI's server side, here in particular ``SailingService[Write]Impl``, and the REST API methods in the ``com.sap.sailing.domain.igtimiadapter.gateway`` bundle implement the necessary permission checks. Furthermore,
TODO Device Auto-Creation/ownerships
## New Riot REST API
### Changes Compared to Original Riot REST API
## Riot/Igtimi Client Connector
### Changes Compared to Original Riot/Igtimi Client Connector