Commit 9e31a38a authored by David Ronnenberg's avatar David Ronnenberg Committed by Martin Grzenia
Browse files

Merge branch 'feature/simple-result-extension' into 'main'

Introduce SimpleResult for generic observation results

See merge request silicon-economy/base/iotbroker/sdk!10
parents 73c749f5 d854103c
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.2.0] - 2021-10-29
### Added
- This CHANGELOG file to keep track of the changes in this project.
- Extend the SensorData model by a SimpleResult type that can be used to describe generic observation results.
### Changed
- Introduce dedicated deserializers for LocationEncodingType and ObservationType instead of handling deserialization of these types in ClassDeserializer.
Since both LocationEncodingType and ObservationType now have a _SIMPLE_ type, "generic" deserialization via the ClassDeserializer is no longer possible.
Furthermore, having dedicated deserializers makes the deserialization process more explicit.
- Improve Javadoc documentation a bit and describe an observation's result more explicitly.
## [5.1.0] - 2021-10-14
This is the first public release.
The IoT Broker SDK (IoT Broker Software Development Kit) facilitates development of components that are intended to be deployed and run in the IoT Broker environment.
This release corresponds to IoT Broker v0.9.
......@@ -11,7 +11,7 @@
<groupId>org.siliconeconomy.iotbroker</groupId>
<artifactId>sdk</artifactId>
<version>5.1.0</version>
<version>5.2.0</version>
<licenses>
<license>
......
......@@ -20,15 +20,17 @@
import static java.util.Objects.requireNonNull;
/**
* An observation describes the act of measuring or otherwise determining the value of an {@link ObservedProperty}.
* An observation describes the act of measuring or otherwise determining the value of an
* {@link ObservedProperty}.
* <p>
* It defines the time the observation happened, the {@link ObservationResult} that contains the actual result of the
* observation and a set of generic parameters that can be used to attach more information to the observation.
* In addition, an observation may contain information about the {@link Location} where the observation happened.
* It defines the time the observation happened, the {@link ObservationResult} that contains the
* actual result of the observation and a set of generic parameters that can be used to attach more
* information to the observation. In addition, an observation may contain information about the
* {@link Location} where the observation happened.
* <p>
* In case the result of the observation could not be determined, the actual result of the observation result can be
* {@code null}. In this case, the observation's parameters can be used to include additional information on why the
* result could not be determined.
* In case the <em>actual result</em> of the observation could not be determined and the
* {@link ObservationResult} contains a {@code null} value, additional information can be included
* in the observation's parameters to describe this circumstance in more detail.
*
* @param <T> The type of the observation result.
* @author C. Hoppe
......@@ -45,17 +47,24 @@
private final Instant timestamp;
/**
* The result of the observation.
* <p>
* Although the result object in an {@link Observation} can not be {@code null}, the <em>actual
* result</em> contained in the {@link ObservationResult} can be {@code null}. This might be the
* case, if the <em>actual result</em> could not be determined.
*/
@NonNull
private final T result;
/**
* The (optional) location where the observation happened.
* The location of an observation may be identical to the location of the device that made the observation. If the
* device that made the observation cannot determine the location of the observation, the value is {@code null}.
* <p>
* The location of an observation may be identical to the location of the device that made the
* observation. If the device that made the observation cannot determine the location of the
* observation, the value is {@code null}.
*/
private final Location<? extends LocationDetails<?>> location;
/**
* A map of generic parameters.
* <p>
* These parameters can be used to attach more information to the observation.
*/
@NonNull
......
......@@ -10,19 +10,21 @@
import org.siliconeconomy.iotbroker.model.sensordata.observation.ObservationType;
/**
* Represents the result of an {@link Observation} for a specific {@link ObservationType}.
* Represents the <em>actual result</em> of an {@link Observation} for a specific
* {@link ObservationType}.
*
* @param <T> The type of the actual result.
* @param <T> The type of the <em>actual result</em>.
* @author C. Hoppe
* @author M. Grzenia
*/
public interface ObservationResult<T> {
/**
* Returns the actual result.
* Can be {@code null}, if the result of an {@link Observation} could not be determined.
* Returns the <em>actual result</em>.
* Can be {@code null}, if the <em>actual result</em> of an {@link Observation} could not be
* determined.
*
* @return The actual result.
* @return The <em>actual result</em>.
*/
@Nullable
T getResult();
......
......@@ -10,11 +10,11 @@
import org.siliconeconomy.iotbroker.model.sensordata.ObservationResult;
/**
* Defines the different types of {@link Observation} and links them to the {@link ObservationResult} sub types they
* are associated with.
* Defines the different types of {@link Observation} and links them to the
* {@link ObservationResult} sub types they are associated with.
* <p>
* During serialization and deserialization, any references to the different observation result sub type classes are
* mapped to the names of the enum elements.
* During serialization and deserialization, any references to the different observation result sub
* type classes are mapped to the names of the enum elements.
*
* @author C. Hoppe
* @author M. Grzenia
......@@ -29,6 +29,10 @@ public enum ObservationType {
* An observation whose result is a floating-point number.
*/
MEASUREMENT(MeasurementResult.class),
/**
* An observation whose result is a string.
*/
SIMPLE(SimpleResult.class),
/**
* An observation whose result is a truth value.
*/
......
/**
* Copyright 2021 Open Logistics Foundation
*
* Licensed under the Open Logistics License 1.0.
* For details on the licensing terms, see the LICENSE file.
*/
package org.siliconeconomy.iotbroker.model.sensordata.observation;
import lombok.EqualsAndHashCode;
import org.siliconeconomy.iotbroker.model.sensordata.ObservationResult;
/**
* Describes an observation result in the simplest and most generic way using a string.
* This is the implementation for the {@link ObservationType#SIMPLE}.
*
* @author M. Grzenia
*/
@EqualsAndHashCode
public class SimpleResult implements ObservationResult<String> {
/**
* The result as a string.
*/
private final String result;
/**
* Creates a new instance.
*
* @param result The result as a string. Can be {@code null}, if the result could not be
* determined.
*/
public SimpleResult(String result) {
this.result = result;
}
@Override
public String getResult() {
return result;
}
}
......@@ -13,8 +13,10 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.siliconeconomy.iotbroker.model.sensordata.*;
import org.siliconeconomy.iotbroker.model.sensordata.location.LatitudeLongitudeDetails;
import org.siliconeconomy.iotbroker.model.sensordata.location.LocationEncodingType;
import org.siliconeconomy.iotbroker.model.sensordata.location.MapcodeDetails;
import org.siliconeconomy.iotbroker.model.sensordata.location.SimpleDetails;
import org.siliconeconomy.iotbroker.model.sensordata.observation.ObservationType;
import org.siliconeconomy.iotbroker.utils.sensordata.deserializer.*;
import org.siliconeconomy.iotbroker.utils.sensordata.serializer.ClassSerializer;
import org.siliconeconomy.iotbroker.utils.sensordata.serializer.LocationDetailsSerializer;
......@@ -24,9 +26,10 @@
* A {@link Module} with already registered serializers and deserializers needed for
* serialization/deserialization of {@link SensorDataMessage} instances.
* <p>
* Note: In addition to registering this module, an {@link ObjectMapper} instance must be configured with the
* {@link SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} disabled and the {@link JavaTimeModule} registered to
* properly serialize/deserialize instances of {@link SensorDataMessage}.
* Note: In addition to registering this module, an {@link ObjectMapper} instance must be configured
* with the {@link SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} disabled and the
* {@link JavaTimeModule} registered to properly serialize/deserialize instances of
* {@link SensorDataMessage}.
*
* @author M. Grzenia
*/
......@@ -39,7 +42,9 @@ public SensorDataModule() {
addDeserializer(Class.class, new ClassDeserializer());
addDeserializer(Datastream.class, new DatastreamDeserializer());
addDeserializer(ObservationType.class, new ObservationTypeDeserializer());
addDeserializer(Location.class, new LocationDeserializer());
addDeserializer(LocationEncodingType.class, new LocationEncodingTypeDeserializer());
// Deserializers for specific location details types of non-primitive type
addDeserializer(LatitudeLongitudeDetails.class, new LongitudeLatitudeDetailsDeserializer());
......
......@@ -11,24 +11,12 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.TextNode;
import org.siliconeconomy.iotbroker.model.sensordata.LocationDetails;
import org.siliconeconomy.iotbroker.model.sensordata.ObservationResult;
import org.siliconeconomy.iotbroker.model.sensordata.location.LocationEncodingType;
import org.siliconeconomy.iotbroker.model.sensordata.observation.ObservationType;
import org.siliconeconomy.iotbroker.utils.sensordata.serializer.ClassSerializer;
import java.io.IOException;
import java.util.Objects;
/**
* Deserializer for instances of {@link Class}.
* <p>
* This deserializer is mainly used for deserialization of
* <ul>
* <li>{@link LocationEncodingType} names and mapping them to the corresponding {@link LocationDetails} sub type</li>
* <li>and {@link ObservationType} names and mapping them to the corresponding {@link ObservationResult} sub type.</li>
* </ul>
* All other classes are deserialized using their canonical name.
* Deserializer for instances of {@link Class} that have been serialized using their canonical name.
*
* @author M. Grzenia
* @see ClassSerializer
......@@ -40,45 +28,18 @@ public ClassDeserializer() {
}
@Override
public Class<?> deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
public Class<?> deserialize(JsonParser jsonParser,
DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
TextNode objectNode = mapper.readTree(jsonParser);
String stringToDeserialize = mapper.treeToValue(objectNode, String.class);
Class<?> result = tryDeserializeLocationDetailsClass(stringToDeserialize);
if (result != null) {
return result;
}
result = tryDeserializeObservationResultClass(stringToDeserialize);
if (result != null) {
return result;
}
try {
return Class.forName(stringToDeserialize);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Could not find class for string: " + stringToDeserialize);
}
}
private Class<?> tryDeserializeLocationDetailsClass(String stringToDeserialize) {
for (LocationEncodingType type : LocationEncodingType.values()) {
if (Objects.equals(stringToDeserialize, type.name())) {
return type.getLocationDetailsClass();
}
}
return null;
}
private Class<?> tryDeserializeObservationResultClass(String stringToDeserialize) {
for (ObservationType type : ObservationType.values()) {
if (Objects.equals(stringToDeserialize, type.name())) {
return type.getObservationResultClass();
}
throw new IllegalArgumentException(String.format(
"Could not find class for string: %s", stringToDeserialize
));
}
return null;
}
}
......@@ -14,6 +14,7 @@
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.siliconeconomy.iotbroker.model.sensordata.*;
import org.siliconeconomy.iotbroker.model.sensordata.observation.ObservationType;
import java.io.IOException;
import java.time.Instant;
......@@ -23,47 +24,53 @@
/**
* Deserializer for instances of {@link Datastream}.
* This deserializer is needed because a datastream contains contextual information about its observations, which in
* turn is needed to properly deserialize the results of the observations.
* This deserializer is needed because a datastream contains contextual information about its
* observations (i.e. the {@link ObservationType}), which in turn is needed to properly deserialize
* the results of the observations.
*
* @author M. Grzenia
*/
@SuppressWarnings("java:S3740")
// The whole purpose of this deserializer is to handle types at runtime
public class DatastreamDeserializer extends StdDeserializer<Datastream<? extends ObservationResult<?>>> {
public class DatastreamDeserializer
extends StdDeserializer<Datastream<? extends ObservationResult<?>>> {
public DatastreamDeserializer() {
super(Datastream.class);
}
@Override
public Datastream<? extends ObservationResult<?>> deserialize(JsonParser jsonParser,
DeserializationContext context) throws IOException {
public Datastream<? extends ObservationResult<?>> deserialize(
JsonParser jsonParser,
DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
ObjectNode objectNode = mapper.readTree(jsonParser);
Class<? extends ObservationResult<?>> observationType
= mapper.treeToValue(objectNode.get("observationType"), Class.class);
ObservationType observationType
= mapper.treeToValue(objectNode.get("observationType"), ObservationType.class);
List<Observation<? extends ObservationResult<?>>> observations = new ArrayList<>();
for (JsonNode observationNode : objectNode.get("observations")) {
observations.add(mapObservation(mapper, observationNode, observationType));
observations.add(
mapObservation(mapper, observationNode, observationType.getObservationResultClass())
);
}
return new Datastream(
mapper.treeToValue(objectNode.get("observedProperty"), ObservedProperty.class),
observationType,
observationType.getObservationResultClass(),
mapper.treeToValue(objectNode.get("unitOfMeasurement"), UnitOfMeasurement.class),
observations
);
}
private Observation<? extends ObservationResult<?>> mapObservation(ObjectMapper mapper,
JsonNode observationNode,
Class<? extends ObservationResult<?>> observationType) throws JsonProcessingException {
private Observation<? extends ObservationResult<?>> mapObservation(
ObjectMapper mapper,
JsonNode observationNode,
Class<? extends ObservationResult<?>> observationResultClass) throws JsonProcessingException {
return new Observation<>(
mapper.treeToValue(observationNode.get("timestamp"), Instant.class),
mapper.treeToValue(observationNode.get("result"), observationType),
mapper.treeToValue(observationNode.get("result"), observationResultClass),
mapper.treeToValue(observationNode.get("location"), Location.class),
mapper.treeToValue(observationNode.get("parameters"), Map.class)
);
......
......@@ -13,6 +13,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.siliconeconomy.iotbroker.model.sensordata.Location;
import org.siliconeconomy.iotbroker.model.sensordata.LocationDetails;
import org.siliconeconomy.iotbroker.model.sensordata.location.LocationEncodingType;
import java.io.IOException;
......@@ -30,19 +31,23 @@ public LocationDeserializer() {
}
@Override
public Location<? extends LocationDetails<?>> deserialize(JsonParser jsonParser,
DeserializationContext context) throws IOException {
public Location<? extends LocationDetails<?>> deserialize(
JsonParser jsonParser,
DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
ObjectNode objectNode = mapper.readTree(jsonParser);
Class<? extends LocationDetails<?>> locationEncodingType
= mapper.treeToValue(objectNode.get("encodingType"), Class.class);
LocationEncodingType locationEncodingType
= mapper.treeToValue(objectNode.get("encodingType"), LocationEncodingType.class);
return new Location(
mapper.treeToValue(objectNode.get("name"), String.class),
mapper.treeToValue(objectNode.get("description"), String.class),
locationEncodingType,
mapper.treeToValue(objectNode.get("details"), locationEncodingType)
locationEncodingType.getLocationDetailsClass(),
mapper.treeToValue(
objectNode.get("details"),
locationEncodingType.getLocationDetailsClass()
)
);
}
}
/**
* Copyright 2021 Open Logistics Foundation
*
* Licensed under the Open Logistics License 1.0.
* For details on the licensing terms, see the LICENSE file.
*/
package org.siliconeconomy.iotbroker.utils.sensordata.deserializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.TextNode;
import org.siliconeconomy.iotbroker.model.sensordata.location.LocationEncodingType;
import org.siliconeconomy.iotbroker.utils.sensordata.serializer.ClassSerializer;
import java.io.IOException;
/**
* Deserializer for instances of {@link LocationEncodingType}.
*
* @author M. Grzenia
* @see ClassSerializer
*/
public class LocationEncodingTypeDeserializer extends StdDeserializer<LocationEncodingType> {
public LocationEncodingTypeDeserializer() {
super(LocationEncodingType.class);
}
@Override
public LocationEncodingType deserialize(JsonParser jsonParser,
DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
TextNode objectNode = mapper.readTree(jsonParser);
String stringToDeserialize = mapper.treeToValue(objectNode, String.class);
return LocationEncodingType.valueOf(stringToDeserialize);
}
}
/**
* Copyright 2021 Open Logistics Foundation
*
* Licensed under the Open Logistics License 1.0.
* For details on the licensing terms, see the LICENSE file.
*/
package org.siliconeconomy.iotbroker.utils.sensordata.deserializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.TextNode;
import org.siliconeconomy.iotbroker.model.sensordata.observation.ObservationType;
import org.siliconeconomy.iotbroker.utils.sensordata.serializer.ClassSerializer;
import java.io.IOException;
/**
* Deserializer for instances of {@link ObservationType}.
*
* @author M. Grzenia
* @see ClassSerializer
*/
public class ObservationTypeDeserializer extends StdDeserializer<ObservationType> {
public ObservationTypeDeserializer() {
super(ObservationType.class);
}
@Override
public ObservationType deserialize(JsonParser jsonParser,
DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
TextNode objectNode = mapper.readTree(jsonParser);
String stringToDeserialize = mapper.treeToValue(objectNode, String.class);
return ObservationType.valueOf(stringToDeserialize);
}
}
......@@ -14,21 +14,28 @@
import org.siliconeconomy.iotbroker.model.sensordata.location.LocationEncodingType;
import org.siliconeconomy.iotbroker.model.sensordata.observation.ObservationType;
import org.siliconeconomy.iotbroker.utils.sensordata.deserializer.ClassDeserializer;
import org.siliconeconomy.iotbroker.utils.sensordata.deserializer.LocationEncodingTypeDeserializer;
import org.siliconeconomy.iotbroker.utils.sensordata.deserializer.ObservationTypeDeserializer;
import java.io.IOException;
/**
* Serializer for instances of {@link Class}.
* <p>
* This serializer is mainly used for serialization of
* Due to some restrictions related to generics, this serializer has to be used for serialization of
* <ul>
* <li>sub types of {@link LocationDetails} and mapping them to the corresponding {@link LocationEncodingType}</li>
* <li>and sub type of {@link ObservationResult} and mapping them to the corresponding {@link ObservationType}.</li>
* <li>sub types of {@link LocationDetails} and mapping them to the corresponding
* {@link LocationEncodingType}</li>
* <li>and sub type of {@link ObservationResult} and mapping them to the corresponding
* {@link ObservationType}.</li>
* </ul>
* <p>
* All other classes are serialized using their canonical name.
*
* @author M. Grzenia
* @see ClassDeserializer
* @see LocationEncodingTypeDeserializer
* @see ObservationTypeDeserializer
*/
@SuppressWarnings("java:S3740")
// The whole purpose of this serializer is to handle types at runtime
......@@ -51,7 +58,8 @@ public void serialize(Class value,
}
}
private void serializeLocationDetailsClass(Class<?> value, JsonGenerator generator) throws IOException {
private void serializeLocationDetailsClass(Class<?> value,
JsonGenerator generator) throws IOException {
for (LocationEncodingType type : LocationEncodingType.values()) {
if (type.getLocationDetailsClass().isAssignableFrom(value)) {
generator.writeString(type.name());
......@@ -59,12 +67,15 @@ private void serializeLocationDetailsClass(Class<?> value, JsonGenerator generat
}
}
throw new IllegalArgumentException(
"Unhandled type of " + LocationDetails.class.getCanonicalName() + ": " + value.getCanonicalName()
);
throw new IllegalArgumentException(String.format(
"Unhandled type of %s: %s",
LocationDetails.class.getCanonicalName(),
value.getCanonicalName()
));
}
private void serializeObservationResultClass(Class<?> value, JsonGenerator generator) throws IOException {
private void serializeObservationResultClass(Class<?> value,
JsonGenerator generator) throws IOException {
for (ObservationType type : ObservationType.values()) {
if (type.getObservationResultClass().isAssignableFrom(value)) {
generator.writeString(type.name());
......@@ -72,8 +83,10 @@ private void serializeObservationResultClass(Class<?> value, JsonGenerator gener
}
}
throw new IllegalArgumentException(
"Unhandled type of " + ObservationResult.class.getCanonicalName() + ": " + value.getCanonicalName()
);
throw new IllegalArgumentException(String.format(
"Unhandled type of %s: %s",
ObservationResult.class.getCanonicalName(),
value.getCanonicalName()
));
}
}
......@@ -15,6 +15,7 @@
import org.siliconeconomy.iotbroker.model.sensordata.location.datatype.Mapcode;
import org.siliconeconomy.iotbroker.model.sensordata.observation.CountResult;
import org.siliconeconomy.iotbroker.model.sensordata.observation.MeasurementResult;
import org.siliconeconomy.iotbroker.model.sensordata.observation.SimpleResult;
import org.siliconeconomy.iotbroker.model.sensordata.observation.TruthResult;