Skip to content

Commit

Permalink
additional real-time notification functionality
Browse files Browse the repository at this point in the history
The added method getGeneralNotificationSubscriber() of the class
NotificationSubscriberProducer creates subscribers supporting all
domain model object types (Alarms, Events, etc.) simultaneously.
This comes with the trade-off of less specificity of the data type
of the objects returned in subscription notifications
(GeneralNotificationRepresentation instead of
AlarmNotificationRepresentation etc.).
  • Loading branch information
Christian-Winter-Software-AG committed Dec 18, 2024
1 parent ceefe21 commit a93e5ee
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private static List<TypeConverter> defaultTypeConverters() {
converters.add(new IDListTypeConverter());
converters.add(new DateConverter());
converters.add(new AuditChangeValueConverter());
converters.add(new DeletedGeneralObjectConverter());
converters.add(new DeletedManagedObjectConverter());
converters.add(new DeletedMeasurementConverter());
converters.add(new DeletedEventConverter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,44 @@
import org.svenson.converter.TypeConverter;

import com.cumulocity.model.idtype.GId;
import com.cumulocity.rest.representation.AbstractExtensibleRepresentation;
import com.cumulocity.rest.representation.alarm.AlarmRepresentation;
import com.cumulocity.rest.representation.event.EventRepresentation;
import com.cumulocity.rest.representation.inventory.ManagedObjectRepresentation;
import com.cumulocity.rest.representation.measurement.MeasurementRepresentation;
import com.cumulocity.rest.representation.operation.OperationRepresentation;

/**
* The converter classes contained in this class are used by Svenson
* for JSON conversion of real-time notification messages to
* {@link com.cumulocity.rest.representation.notification.NotificationRepresentation
* NotificationRepresentation}s.
* The conversion implemented here is needed for notifications on
* {@code DELETE} operations. In these cases, just a string denoting the ID
* of the deleted object is transferred as notification message data attribute.
* The provided converters wrap such IDs into objects of the desired type.
*/
public class NotificationConverters {

public static class DeletedGeneralObjectConverter implements TypeConverter {

@Override
public Object fromJSON(Object o) {
if (o instanceof String) {
AbstractExtensibleRepresentation wrapper = new AbstractExtensibleRepresentation();
wrapper.set(new GId((String)o));
o = wrapper;
}
return o;
}

@Override
public Object toJSON(Object o) { // not needed at all
return o;
}

}

public static class DeletedManagedObjectConverter implements TypeConverter {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,27 @@ private Object getDataOrId() {
// and in case of realtimeAction == null
}

/**
* This class exists for technical purpose and is used in cases
* where real-time notifications of any type are expected,
* but using Generics as with
* {@code NotificationRepresentation<AbstractExtensibleRepresentation>}
* is unsuitable.
*
* @see NotificationRepresentation
*/
public static class GeneralNotificationRepresentation
extends NotificationRepresentation<AbstractExtensibleRepresentation> {

@Override
@JSONProperty
@JSONConverter(type = DeletedGeneralObjectConverter.class)
public AbstractExtensibleRepresentation getData() {
return super.getData();
}

}

/**
* This class is the Java representation of real-time notifications for
* {@linkplain ManagedObjectRepresentation Managed Objects}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.cumulocity.model;

import java.util.HashMap;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
Expand All @@ -18,39 +20,67 @@ public class NotificationConversionTest {

private static final JSONParser PARSER = JSONBase.getJSONParser();

public <T extends NotificationRepresentation<?>> void jsonRoundtrip(String json, Class<T> type) {
public void shallowJsonRoundtripCheck(String json) {
GeneralNotificationRepresentation parsed =
PARSER.parse(GeneralNotificationRepresentation.class, json);
String json2 = parsed.toJSON(); // This one might differ from the input JSON object
// because the input might not be in the canonical
// form (whitespace normalization).

// check for equality in terms of JSON structure
assertEquals(PARSER.parse(json), PARSER.parse(json2));
// Note: We can't compare objects of type GeneralNotificationRepresentation
// because the underlying AbstractExtensibleRepresentation does not have
// a semantic "equals" method.
}

public <T extends NotificationRepresentation<?>> void semanticJsonRoundtripCheck(String json, Class<T> type) {
T parsed = PARSER.parse(type, json);
String json2 = parsed.toJSON(); // might differ from input JSON object because
// input might not be in the canonical form
String json2 = parsed.toJSON(); // This one might differ from the input JSON object
// because the input might not be in the canonical
// form (whitespace, but also date-time strings,
// and maybe other things).
T parsed2 = PARSER.parse(type, json2);

// check for equality in terms of target object type
assertEquals(parsed, parsed2);
}

@Test
public void testOperationNotification() {
jsonRoundtrip("{ \"realtimeAction\": \"CREATE\", \"data\": { \"id\": \"0815\","
+ "\"self\": \"https://TENANT.DOMAIN/devicecontrol/operation/0815\","
+ "\"creationTime\": \"2019-12-17T09:46:45.435+01:00\", \"deviceId\": \"42\","
+ "\"deviceName\": \"My Device\", \"status\": \"PENDING\", \"time\":"
+ "\"2020-01-01T00:00:00+01:00\", \"c8y_Restart\": {} } }",
String operationNotification =
"{ \"realtimeAction\": \"CREATE\", \"data\": { \"id\": \"0815\", "
+ "\"self\": \"https://TENANT.DOMAIN/devicecontrol/operation/0815\", "
+ "\"creationTime\": \"2019-12-17T09:46:45.435+01:00\", \"deviceId\": "
+ "\"42\", \"deviceName\": \"My Device\", \"status\": \"PENDING\", "
+ "\"time\": \"2020-01-01T00:00:00+01:00\", \"c8y_Restart\": {} } }";
shallowJsonRoundtripCheck(operationNotification);
semanticJsonRoundtripCheck(operationNotification,
OperationNotificationRepresentation.class);
}

@Test
public void testDeletedOperationNotification() {
jsonRoundtrip("{ \"realtimeAction\": \"DELETE\", \"data\": \"0815\" }",
String deletedOperationNotification =
"{ \"realtimeAction\": \"DELETE\", \"data\": \"0815\" }";
shallowJsonRoundtripCheck(deletedOperationNotification);
semanticJsonRoundtripCheck(deletedOperationNotification,
OperationNotificationRepresentation.class);
}

@Test
public void testEventNotificationToJson() {
jsonRoundtrip("{ \"realtimeAction\": \"CREATE\", \"data\": { \"id\": \"1234\","
+ "\"self\": \"https://TENANT.DOMAIN/event/events/1234\", \"creationTime\":"
+ "\"2020-01-01T00:00:01.184+01:00\", \"lastUpdated\":"
+ "\"2020-01-01T00:00:01.184+01:00\", \"source\": { \"id\": \"42\","
+ "\"self\": \"https://TENANT.DOMAIN/inventory/managedObjects/42\" },"
+ "\"type\": \"CustomEvent\", \"time\": \"2020-01-01T00:00:00+01:00\","
+ "\"text\": \"This even occurred.\" } }",
public void testEventNotification() {
String eventNotification =
"{ \"realtimeAction\": \"CREATE\", \"data\": { \"id\": \"1234\", "
+ "\"self\": \"https://TENANT.DOMAIN/event/events/1234\", "
+ "\"creationTime\": \"2020-01-01T00:00:01.184+01:00\", "
+ "\"lastUpdated\": \"2020-01-01T00:00:01.184+01:00\", "
+ "\"source\": { \"id\": \"42\", \"self\": "
+ "\"https://TENANT.DOMAIN/inventory/managedObjects/42\" }, "
+ "\"type\": \"CustomEvent\", \"time\": \"2020-01-01T00:00:00+01:00\", "
+ "\"text\": \"This event occurred.\" } }";
shallowJsonRoundtripCheck(eventNotification);
semanticJsonRoundtripCheck(eventNotification,
EventNotificationRepresentation.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,61 @@ Subscriber<GId, T> getSubscriber(Endpoint endpoint, NotificationType<?,T> type)
.build();
}

/**
* This class is used for identifying real-time notification
* subscription channels of the endpoint “{@code notification/realtime}”
* in the context of subscribers produced by
* {@link NotificationSubscriberProducer#getGeneralSubscriber()}.
*/
public static final class RealtimeChannel {
public final NotificationType<?,?> type;
public final GId id;
private final String channelString;

public RealtimeChannel(NotificationType<?,?> type, GId id) {
this.type = type;
this.id = id;
String prefix = Endpoint.RealtimeNotifications.supportedChannelPrefixes.get(type);
if (prefix == null)
throw new IllegalArgumentException("Unsupported type " + type + ".");
this.channelString = prefix + id.getValue();
}

@Override
public String toString() {
return channelString;
}
}

/**
* Creates a real-time notifications subscriber for the endpoint
* “{@code notification/realtime}”. Such a subscriber can subscribe
* to notifications for any notification-aware type of domain model object.
* The subscription channel must be indicated at each subscribe operation:
* <pre>{@code
* producer.getGeneralSubscriber()
* .subscribe(new RealtimeChannel(NotificationType.ALARM, new GId("*")),
* new SubscriptionListener<>() { ... });
* }</pre>
* While subscribers produced by this method are not bound to creating
* subscriptions for only one specific type of notification (e.&thinsp;g.
* alarm notifications), this comes with the trade-off of less specificity
* of the data type of the objects returned in subscription notifications
* ({@code GeneralNotificationRepresentation} instead of e.&thinsp;g.
* {@code AlarmNotificationRepresentation}).
*
* @return a subscriber with characteristics as described above
*/
public Subscriber<RealtimeChannel, GeneralNotificationRepresentation>
getGeneralSubscriber() {

return new SubscriberBuilder<RealtimeChannel, GeneralNotificationRepresentation>()
.withParameters(parameters)
.withEndpoint(Endpoint.RealtimeNotifications.path)
.withSubscriptionNameResolver(channel -> channel.toString())
.withDataType(GeneralNotificationRepresentation.class)
.withMessageDeliveryAcknowlage(true)
.build();
}

}

0 comments on commit a93e5ee

Please sign in to comment.