@@ -265,6 +265,68 @@ public void thingMergedWithOnlyNullValues() {
265265 "}" ));
266266 }
267267
268+ @ Test
269+ public void thingMergedTracksDeletedFieldsFromNullValues () {
270+ enableDeletedFields ();
271+ final JsonObject mergedObject = JsonObject .of ("{\n " +
272+ " \" attributes\" : {\n " +
273+ " \" location\" : null,\n " +
274+ " \" status\" : \" active\" \n " +
275+ " },\n " +
276+ " \" features\" : {\n " +
277+ " \" sensor\" : {\n " +
278+ " \" properties\" : {\n " +
279+ " \" temp\" : null,\n " +
280+ " \" humidity\" : 50\n " +
281+ " }\n " +
282+ " }\n " +
283+ " }\n " +
284+ "}" );
285+ final ThingMerged event = ThingMerged .of (ThingId .of ("thing:merged" ), JsonPointer .empty (), mergedObject ,
286+ 1L , Instant .ofEpochSecond (1L ), DittoHeaders .empty (), null );
287+
288+ final Adaptable adaptable = ADAPTER .toAdaptable (event );
289+
290+ Assertions .assertThat (mapToJson (adaptable ))
291+ .isEqualTo (JsonObject .of ("{\n " +
292+ " \" thingId\" : \" thing:merged\" ,\n " +
293+ " \" attributes\" : {\n " +
294+ " \" status\" : \" active\" \n " +
295+ " },\n " +
296+ " \" features\" : {\n " +
297+ " \" sensor\" : {\n " +
298+ " \" properties\" : {\n " +
299+ " \" humidity\" : 50\n " +
300+ " }\n " +
301+ " }\n " +
302+ " },\n " +
303+ " \" _modified\" : \" 1970-01-01T00:00:01Z\" ,\n " +
304+ " \" _revision\" : 1,\n " +
305+ " \" _deletedFields\" : {\n " +
306+ " \" attributes\" : {\n " +
307+ " \" location\" : \" 1970-01-01T00:00:01Z\" \n " +
308+ " },\n " +
309+ " \" features\" : {\n " +
310+ " \" sensor\" : {\n " +
311+ " \" properties\" : {\n " +
312+ " \" temp\" : \" 1970-01-01T00:00:01Z\" \n " +
313+ " }\n " +
314+ " }\n " +
315+ " }\n " +
316+ " },\n " +
317+ " \" _context\" : {\n " +
318+ " \" topic\" : \" thing/merged/things/twin/events/merged\" ,\n " +
319+ " \" path\" : \" /\" ,\n " +
320+ " \" value\" :{\" attributes\" :{\" status\" :\" active\" },\" features\" :{\" sensor\" :{\" properties\" :{\" humidity\" :50}}}},\n " +
321+ " \" headers\" : {\n " +
322+ " \" entity-revision\" : \" 1\" ,\n " +
323+ " \" response-required\" : \" false\" ,\n " +
324+ " \" content-type\" : \" application/merge-patch+json\" \n " +
325+ " }\n " +
326+ " }\n " +
327+ "}" ));
328+ }
329+
268330 @ Test
269331 public void thingMergedWithExtraFields () {
270332 final var thingId = ThingId .of ("the.namespace:the-thing-id" );
@@ -523,15 +585,125 @@ public void thingDeletedIsMappedWithDeletedTimestamp() {
523585 }
524586
525587 @ Test
526- public void deletedEventsAreNotMapped () {
527- assertNotMapped (AttributeDeleted .of (ThingId .of ("thing:id" ), JsonPointer .of ("/the/quick/brown/fox/ " ), 3L ,
588+ public void deletedEventsAreNotMappedByDefault () {
589+ assertNotMapped (AttributeDeleted .of (ThingId .of ("thing:id" ), JsonPointer .of ("/the/quick/brown/fox" ), 3L ,
528590 Instant .ofEpochSecond (3L ), DittoHeaders .empty (), null ));
529591 assertNotMapped (FeaturePropertyDeleted .of (ThingId .of ("thing:id" ), "featureId" ,
530592 JsonPointer .of ("jumps/over/the/lazy/dog" ), 4L , Instant .ofEpochSecond (4L ), DittoHeaders .empty (), null ));
531593 assertNotMapped (FeatureDeleted .of (ThingId .of ("thing:id" ), "featureId" , 5L , Instant .EPOCH ,
532594 DittoHeaders .empty (), null ));
533595 }
534596
597+ @ Test
598+ public void deletedEventsAreMappedWithDeletedFieldsWhenEnabled () {
599+ enableDeletedFields ();
600+ final AttributeDeleted attributeDeleted = AttributeDeleted .of (
601+ ThingId .of ("thing:id" ),
602+ JsonPointer .of ("/the/quick/brown/fox" ),
603+ 3L ,
604+ Instant .ofEpochSecond (3L ),
605+ DittoHeaders .empty (),
606+ null );
607+
608+ Assertions .assertThat (mapToJson (ADAPTER .toAdaptable (attributeDeleted )))
609+ .isEqualTo (JsonObject .of ("{\n " +
610+ " \" thingId\" : \" thing:id\" ,\n " +
611+ " \" _modified\" : \" 1970-01-01T00:00:03Z\" ,\n " +
612+ " \" _revision\" : 3,\n " +
613+ " \" _deletedFields\" : {\n " +
614+ " \" attributes\" : {\n " +
615+ " \" the\" : {\n " +
616+ " \" quick\" : {\n " +
617+ " \" brown\" : {\n " +
618+ " \" fox\" : \" 1970-01-01T00:00:03Z\" \n " +
619+ " }\n " +
620+ " }\n " +
621+ " }\n " +
622+ " }\n " +
623+ " },\n " +
624+ " \" _context\" : {\n " +
625+ " \" topic\" : \" thing/id/things/twin/events/deleted\" ,\n " +
626+ " \" path\" : \" /attributes/the/quick/brown/fox\" ,\n " +
627+ " \" value\" : null,\n " +
628+ " \" headers\" : {\n " +
629+ " \" entity-revision\" : \" 3\" ,\n " +
630+ " \" response-required\" : \" false\" \n " +
631+ " }\n " +
632+ " }\n " +
633+ "}" ));
634+
635+ final FeaturePropertyDeleted propertyDeleted = FeaturePropertyDeleted .of (
636+ ThingId .of ("thing:id" ),
637+ "featureId" ,
638+ JsonPointer .of ("jumps/over/the/lazy/dog" ),
639+ 4L ,
640+ Instant .ofEpochSecond (4L ),
641+ DittoHeaders .empty (),
642+ null );
643+
644+ Assertions .assertThat (mapToJson (ADAPTER .toAdaptable (propertyDeleted )))
645+ .isEqualTo (JsonObject .of ("{\n " +
646+ " \" thingId\" : \" thing:id\" ,\n " +
647+ " \" _modified\" : \" 1970-01-01T00:00:04Z\" ,\n " +
648+ " \" _revision\" : 4,\n " +
649+ " \" _deletedFields\" : {\n " +
650+ " \" features\" : {\n " +
651+ " \" featureId\" : {\n " +
652+ " \" properties\" : {\n " +
653+ " \" jumps\" : {\n " +
654+ " \" over\" : {\n " +
655+ " \" the\" : {\n " +
656+ " \" lazy\" : {\n " +
657+ " \" dog\" : \" 1970-01-01T00:00:04Z\" \n " +
658+ " }\n " +
659+ " }\n " +
660+ " }\n " +
661+ " }\n " +
662+ " }\n " +
663+ " }\n " +
664+ " }\n " +
665+ " },\n " +
666+ " \" _context\" : {\n " +
667+ " \" topic\" : \" thing/id/things/twin/events/deleted\" ,\n " +
668+ " \" path\" : \" /features/featureId/properties/jumps/over/the/lazy/dog\" ,\n " +
669+ " \" value\" : null,\n " +
670+ " \" headers\" : {\n " +
671+ " \" entity-revision\" : \" 4\" ,\n " +
672+ " \" response-required\" : \" false\" \n " +
673+ " }\n " +
674+ " }\n " +
675+ "}" ));
676+
677+ final FeatureDeleted featureDeleted = FeatureDeleted .of (
678+ ThingId .of ("thing:id" ),
679+ "featureId" ,
680+ 5L ,
681+ Instant .EPOCH ,
682+ DittoHeaders .empty (),
683+ null );
684+
685+ Assertions .assertThat (mapToJson (ADAPTER .toAdaptable (featureDeleted )))
686+ .isEqualTo (JsonObject .of ("{\n " +
687+ " \" thingId\" : \" thing:id\" ,\n " +
688+ " \" _modified\" : \" 1970-01-01T00:00:00Z\" ,\n " +
689+ " \" _revision\" : 5,\n " +
690+ " \" _deletedFields\" : {\n " +
691+ " \" features\" : {\n " +
692+ " \" featureId\" : \" 1970-01-01T00:00:00Z\" \n " +
693+ " }\n " +
694+ " },\n " +
695+ " \" _context\" : {\n " +
696+ " \" topic\" : \" thing/id/things/twin/events/deleted\" ,\n " +
697+ " \" path\" : \" /features/featureId\" ,\n " +
698+ " \" value\" : null,\n " +
699+ " \" headers\" : {\n " +
700+ " \" entity-revision\" : \" 5\" ,\n " +
701+ " \" response-required\" : \" false\" \n " +
702+ " }\n " +
703+ " }\n " +
704+ "}" ));
705+ }
706+
535707 @ Test
536708 public void nonThingEventsAreNotMapped () {
537709 // command
@@ -553,6 +725,14 @@ private void assertNotMapped(final Signal signal) {
553725 assertThat (underTest .map (ADAPTER .toAdaptable (signal ))).isEmpty ();
554726 }
555727
728+ private void enableDeletedFields () {
729+ final Map <String , JsonValue > options = Map .of (
730+ NormalizedMessageMapper .INCLUDE_DELETED_FIELDS , JsonValue .of ("true" ));
731+ underTest .configure (connection , connectivityConfig ,
732+ DefaultMessageMapperConfiguration .of ("normalizer" , options , Map .of (), Map .of ()),
733+ actorSystem );
734+ }
735+
556736 private JsonObject mapToJson (final Adaptable message ) {
557737 return underTest .map (message )
558738 .stream ()
0 commit comments