diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappableOutboundSignal.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappableOutboundSignal.java index 05a62ef369..0d2f8318a8 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappableOutboundSignal.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappableOutboundSignal.java @@ -17,16 +17,17 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Predicate; import javax.annotation.Nullable; -import org.eclipse.ditto.json.JsonField; -import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.base.model.signals.Signal; import org.eclipse.ditto.connectivity.model.PayloadMapping; import org.eclipse.ditto.connectivity.model.Target; -import org.eclipse.ditto.base.model.signals.Signal; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; /** * Represent an outbound signal that is ready to be mapped with the given {@link org.eclipse.ditto.connectivity.model.PayloadMapping}. @@ -65,6 +66,11 @@ public List getTargets() { return delegate.getTargets(); } + @Override + public Optional getExtra() { + return delegate.getExtra(); + } + @Override public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate thePredicate) { // the externalMessage is omitted as this should not be required to go over the wire diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappedOutboundSignal.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappedOutboundSignal.java index f9fde3de1d..f5af1bb4a3 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappedOutboundSignal.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MappedOutboundSignal.java @@ -14,14 +14,15 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Predicate; -import org.eclipse.ditto.json.JsonField; -import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.base.model.signals.Signal; import org.eclipse.ditto.connectivity.model.Target; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.protocol.Adaptable; -import org.eclipse.ditto.base.model.signals.Signal; /** * Represent an outbound signal that was mapped to an external message. It wraps the original signal, the mapped @@ -60,6 +61,11 @@ public List getTargets() { return delegate.getTargets(); } + @Override + public Optional getExtra() { + return delegate.getExtra(); + } + @Override public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate thePredicate) { // the externalMessage is omitted as this should not be required to go over the wire diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MultiMappedOutboundSignal.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MultiMappedOutboundSignal.java index 191d4a184b..7d2392db5c 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MultiMappedOutboundSignal.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/MultiMappedOutboundSignal.java @@ -21,13 +21,13 @@ import javax.annotation.Nullable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.base.model.signals.Signal; +import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonField; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; -import org.eclipse.ditto.connectivity.model.Target; -import org.eclipse.ditto.base.model.signals.Signal; import akka.actor.ActorRef; @@ -75,6 +75,11 @@ public List getTargets() { return first().getTargets(); } + @Override + public Optional getExtra() { + return first().getExtra(); + } + @Override public List getMappedOutboundSignals() { return outboundSignals; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessorActor.java index 869909f8b5..3b4b1833dc 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessorActor.java @@ -72,6 +72,7 @@ import org.eclipse.ditto.connectivity.service.messaging.validation.ConnectionValidator; import org.eclipse.ditto.connectivity.service.util.ConnectivityMdcEntryKey; import org.eclipse.ditto.edge.service.headers.DittoHeadersValidator; +import org.eclipse.ditto.edge.service.placeholders.ThingJsonPlaceholder; import org.eclipse.ditto.internal.models.signalenrichment.SignalEnrichmentFacade; import org.eclipse.ditto.internal.utils.akka.controlflow.AbstractGraphActor; import org.eclipse.ditto.internal.utils.akka.logging.ThreadSafeDittoLoggingAdapter; @@ -96,6 +97,7 @@ import org.eclipse.ditto.rql.query.criteria.Criteria; import org.eclipse.ditto.rql.query.filter.QueryFilterCriteriaFactory; import org.eclipse.ditto.rql.query.things.ThingPredicateVisitor; +import org.eclipse.ditto.things.model.Thing; import org.eclipse.ditto.things.model.ThingFieldSelector; import org.eclipse.ditto.things.model.ThingId; import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingNotAccessibleException; @@ -137,6 +139,7 @@ public final class OutboundMappingProcessorActor private static final TopicPathPlaceholder TOPIC_PATH_PLACEHOLDER = TopicPathPlaceholder.getInstance(); private static final ResourcePlaceholder RESOURCE_PLACEHOLDER = ResourcePlaceholder.getInstance(); private static final TimePlaceholder TIME_PLACEHOLDER = TimePlaceholder.getInstance(); + private static final ThingJsonPlaceholder THING_JSON_PLACEHOLDER = ThingJsonPlaceholder.getInstance(); private final ActorRef clientActor; private final Connection connection; @@ -791,8 +794,13 @@ private Collection applyFilter(final OutboundSignalWit return outboundSignalWithExtra.getExtra() .flatMap(extra -> ThingEventToThingConverter .mergeThingWithExtraFields(signal, extraFields.get(), extra) - .filter(ThingPredicateVisitor.apply(criteria, topicPathPlaceholderResolver, - resourcePlaceholderResolver, timePlaceholderResolver)) + .filter(thing -> { + final PlaceholderResolver thingPlaceholderResolver = PlaceholderFactory + .newPlaceholderResolver(THING_JSON_PLACEHOLDER, thing); + return ThingPredicateVisitor.apply(criteria, topicPathPlaceholderResolver, + resourcePlaceholderResolver, timePlaceholderResolver, thingPlaceholderResolver) + .test(thing); + }) .map(thing -> outboundSignalWithExtra)) .map(Collections::singletonList) .orElse(List.of()); @@ -938,7 +946,7 @@ private OutboundSignalWithSender setFailedEnrichment(final DittoRuntimeException sender, Pair.apply(e, t), extra); } - private OutboundSignalWithSender setExtra(final JsonObject extra) { + public OutboundSignalWithSender setExtra(final JsonObject extra) { return new OutboundSignalWithSender( OutboundSignalFactory.newOutboundSignal(delegate.getSource(), getTargets()), sender, enrichmentFailure, extra diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/Resolvers.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/Resolvers.java index dc3a60dda1..8f45bc93ad 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/Resolvers.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/Resolvers.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; @@ -26,8 +27,10 @@ import org.eclipse.ditto.base.model.signals.Signal; import org.eclipse.ditto.connectivity.api.ExternalMessage; import org.eclipse.ditto.connectivity.api.OutboundSignal; -import org.eclipse.ditto.connectivity.service.placeholders.ConnectivityPlaceholders; import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.service.placeholders.ConnectivityPlaceholders; +import org.eclipse.ditto.json.JsonFieldSelector; +import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.placeholders.ExpressionResolver; import org.eclipse.ditto.placeholders.Placeholder; import org.eclipse.ditto.placeholders.PlaceholderFactory; @@ -35,6 +38,7 @@ import org.eclipse.ditto.protocol.Adaptable; import org.eclipse.ditto.protocol.TopicPath; import org.eclipse.ditto.protocol.adapter.DittoProtocolAdapter; +import org.eclipse.ditto.things.model.signals.events.ThingEventToThingConverter; /** * Creator of expression resolvers for incoming and outgoing messages. @@ -52,17 +56,25 @@ private Resolvers() { */ private static final List> RESOLVER_CREATORS = Arrays.asList( // For incoming messages, header mapping injects headers of external messages into Ditto headers. - ResolverCreator.of(PlaceholderFactory.newHeadersPlaceholder(), (e, s, t, a, c) -> e), + ResolverCreator.of(PlaceholderFactory.newHeadersPlaceholder(), (h, s, t, a, c, e) -> h), ResolverCreator.of(ConnectivityPlaceholders.newEntityPlaceholder(), - (e, s, t, a, c) -> WithEntityId.getEntityIdOfType(EntityId.class, s).orElse(null)), + (h, s, t, a, c, e) -> WithEntityId.getEntityIdOfType(EntityId.class, s).orElse(null)), ResolverCreator.of(ConnectivityPlaceholders.newThingPlaceholder(), - (e, s, t, a, c) -> WithEntityId.getEntityIdOfType(EntityId.class, s).orElse(null)), - ResolverCreator.of(ConnectivityPlaceholders.newFeaturePlaceholder(), (e, s, t, a, c) -> s), - ResolverCreator.of(ConnectivityPlaceholders.newTopicPathPlaceholder(), (e, s, t, a, c) -> t), - ResolverCreator.of(ConnectivityPlaceholders.newResourcePlaceholder(), (e, s, t, a, c) -> s), - ResolverCreator.of(ConnectivityPlaceholders.newTimePlaceholder(), (e, s, t, a, c) -> new Object()), - ResolverCreator.of(ConnectivityPlaceholders.newRequestPlaceholder(), (e, s, t, a, c) -> a), - ResolverCreator.of(ConnectivityPlaceholders.newConnectionIdPlaceholder(), (e, s, t, a, c) -> c) + (h, s, t, a, c, e) -> WithEntityId.getEntityIdOfType(EntityId.class, s).orElse(null)), + ResolverCreator.of(ConnectivityPlaceholders.newThingJsonPlaceholder(), + (h, s, t, a, c, e) -> ThingEventToThingConverter + .mergeThingWithExtraFields(s, + JsonFieldSelector.newInstance(""), + Optional.ofNullable(e).orElse(JsonObject.empty()) + ) + .orElse(null) + ), + ResolverCreator.of(ConnectivityPlaceholders.newFeaturePlaceholder(), (h, s, t, a, c, e) -> s), + ResolverCreator.of(ConnectivityPlaceholders.newTopicPathPlaceholder(), (h, s, t, a, c, e) -> t), + ResolverCreator.of(ConnectivityPlaceholders.newResourcePlaceholder(), (h, s, t, a, c, e) -> s), + ResolverCreator.of(ConnectivityPlaceholders.newTimePlaceholder(), (h, s, t, a, c, e) -> new Object()), + ResolverCreator.of(ConnectivityPlaceholders.newRequestPlaceholder(), (h, s, t, a, c, e) -> a), + ResolverCreator.of(ConnectivityPlaceholders.newConnectionIdPlaceholder(), (h, s, t, a, c, e) -> c) ); private static final List> PLACEHOLDERS = Collections.unmodifiableList( @@ -94,16 +106,21 @@ public static ExpressionResolver forOutbound(final OutboundSignal.Mapped mappedO final Adaptable adaptable = mappedOutboundSignal.getAdaptable(); return PlaceholderFactory.newExpressionResolver( RESOLVER_CREATORS.stream() - .map(creator -> creator.create(adaptable.getDittoHeaders(), signal, + .map(creator -> creator.create(adaptable.getDittoHeaders(), + signal, externalMessage.getTopicPath().orElse(null), signal.getDittoHeaders().getAuthorizationContext(), - sendingConnectionId)) + sendingConnectionId, + mappedOutboundSignal.getExtra() + .or(() -> mappedOutboundSignal.getAdaptable().getPayload().getExtra()) + .orElse(null) + )) .toArray(PlaceholderResolver[]::new) ); } /** - * Create an expression resolver for an signal. + * Create an expression resolver for a signal. * * @param signal the signal. * @param connectionId the ID of the connection that handles the signal @@ -114,10 +131,13 @@ public static ExpressionResolver forSignal(final Signal signal, final ConnectionId connectionId) { return PlaceholderFactory.newExpressionResolver( RESOLVER_CREATORS.stream() - .map(creator -> creator.create(signal.getDittoHeaders(), signal, + .map(creator -> creator.create(signal.getDittoHeaders(), + signal, PROTOCOL_ADAPTER.toTopicPath(signal), signal.getDittoHeaders().getAuthorizationContext(), - connectionId)) + connectionId, + null + )) .toArray(PlaceholderResolver[]::new) ); } @@ -135,10 +155,13 @@ public static ExpressionResolver forExternalMessage(final ExternalMessage messag return PlaceholderFactory.newExpressionResolver( RESOLVER_CREATORS.stream() - .map(creator -> creator.create(makeCaseInsensitive(message.getHeaders()), null, + .map(creator -> creator.create(makeCaseInsensitive(message.getHeaders()), + null, message.getTopicPath().orElse(null), message.getAuthorizationContext().orElse(null), - receivingConnectionId)) + receivingConnectionId, + null + )) .toArray(PlaceholderResolver[]::new) ); } @@ -151,8 +174,8 @@ public static ExpressionResolver forExternalMessage(final ExternalMessage messag * @return the expression resolver. * @since 1.2.0 */ - public static ExpressionResolver forOutboundSignal(final OutboundSignal.Mappable outboundSignal, final - ConnectionId sendingConnectionId) { + public static ExpressionResolver forOutboundSignal(final OutboundSignal.Mappable outboundSignal, + final ConnectionId sendingConnectionId) { return PlaceholderFactory.newExpressionResolver( RESOLVER_CREATORS.stream() @@ -160,7 +183,9 @@ public static ExpressionResolver forOutboundSignal(final OutboundSignal.Mappable outboundSignal.getSource(), PROTOCOL_ADAPTER.toTopicPath(outboundSignal.getSource()), outboundSignal.getSource().getDittoHeaders().getAuthorizationContext(), - sendingConnectionId)) + sendingConnectionId, + outboundSignal.getExtra().orElse(null) + )) .toArray(PlaceholderResolver[]::new) ); } @@ -180,9 +205,13 @@ public static ExpressionResolver forInbound(final ExternalMessage externalMessag @Nullable final ConnectionId connectionId) { return PlaceholderFactory.newExpressionResolver( RESOLVER_CREATORS.stream() - .map(creator -> - creator.create(makeCaseInsensitive(externalMessage.getHeaders()), signal, topicPath, - authorizationContext, connectionId)) + .map(creator -> creator.create(makeCaseInsensitive(externalMessage.getHeaders()), + signal, + topicPath, + authorizationContext, + connectionId, + null + )) .toArray(PlaceholderResolver[]::new) ); } @@ -200,8 +229,9 @@ private static DittoHeaders makeCaseInsensitive(final Map extern private interface ResolverDataExtractor { @Nullable - T extract(Map inputHeaders, @Nullable Signal signal, @Nullable TopicPath topicPath, - @Nullable AuthorizationContext authorizationContext, @Nullable final ConnectionId connectionId); + T extract(Map headers, @Nullable Signal signal, @Nullable TopicPath topicPath, + @Nullable AuthorizationContext authorizationContext, @Nullable ConnectionId connectionId, + @Nullable JsonObject extraFields); } /** @@ -228,9 +258,10 @@ private static ResolverCreator of(final Placeholder placeholder, private PlaceholderResolver create(final Map inputHeaders, @Nullable final Signal signal, @Nullable final TopicPath topicPath, @Nullable final AuthorizationContext authorizationContext, - @Nullable ConnectionId connectionId) { - return PlaceholderFactory.newPlaceholderResolver(placeholder, - dataExtractor.extract(inputHeaders, signal, topicPath, authorizationContext, connectionId)); + @Nullable final ConnectionId connectionId, @Nullable final JsonObject extraFields) { + return PlaceholderFactory.newPlaceholderResolver(placeholder, dataExtractor.extract( + inputHeaders, signal, topicPath, authorizationContext, connectionId, extraFields + )); } private Placeholder getPlaceholder() { diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/placeholders/ConnectivityPlaceholders.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/placeholders/ConnectivityPlaceholders.java index 55355cae63..0f313887bd 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/placeholders/ConnectivityPlaceholders.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/placeholders/ConnectivityPlaceholders.java @@ -16,6 +16,7 @@ import org.eclipse.ditto.edge.service.placeholders.FeaturePlaceholder; import org.eclipse.ditto.edge.service.placeholders.PolicyPlaceholder; import org.eclipse.ditto.edge.service.placeholders.RequestPlaceholder; +import org.eclipse.ditto.edge.service.placeholders.ThingJsonPlaceholder; import org.eclipse.ditto.edge.service.placeholders.ThingPlaceholder; import org.eclipse.ditto.placeholders.TimePlaceholder; import org.eclipse.ditto.protocol.placeholders.ResourcePlaceholder; @@ -34,6 +35,13 @@ public static ThingPlaceholder newThingPlaceholder() { return ThingPlaceholder.getInstance(); } + /** + * @return the singleton instance of {@link ThingJsonPlaceholder} + */ + public static ThingJsonPlaceholder newThingJsonPlaceholder() { + return ThingJsonPlaceholder.getInstance(); + } + /** * @return the singleton instance of {@link PolicyPlaceholder} */ diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/ResolversTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/ResolversTest.java index fdaba112d9..b6e1696908 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/ResolversTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/ResolversTest.java @@ -30,17 +30,40 @@ import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.model.Topic; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.placeholders.ExpressionResolver; import org.eclipse.ditto.protocol.Adaptable; import org.eclipse.ditto.protocol.adapter.DittoProtocolAdapter; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingId; import org.eclipse.ditto.things.model.signals.events.ThingDeleted; import org.eclipse.ditto.things.model.signals.events.ThingEvent; +import org.eclipse.ditto.things.model.signals.events.ThingMerged; import org.junit.Test; public final class ResolversTest { private static final ConnectionId CONNECTION_ID = ConnectionId.generateRandom(); + private static final Thing KNOWN_THING = Thing.newBuilder() + .setId(ThingId.of("org.eclipse.ditto", "resolver-test-1")) + .setAttributes(Attributes.newBuilder() + .set("one", 1) + .set("obj", JsonObject.newBuilder() + .set("a", "b") + .build() + ) + .set("array", JsonArray.newBuilder() + .add(1, 2, 3) + .build() + ) + .build() + ) + .build(); + @Test public void resolveTargetAddressWithEntityIdPlaceholder() { final AcknowledgementLabel acknowledgementLabel = AcknowledgementLabel.of("please-verify"); @@ -74,4 +97,40 @@ public void resolveTargetAddressWithEntityIdPlaceholder() { assertThat(resolvedOptional).contains("hono.command.my_tenant/" + TestConstants.Things.THING_ID); } + + @Test + public void resolveThingContentWithThingJsonPlaceholder() { + final AcknowledgementLabel acknowledgementLabel = AcknowledgementLabel.of("please-verify"); + final DittoHeaders dittoHeaders = DittoHeaders.newBuilder() + .correlationId(TestConstants.CORRELATION_ID) + .putHeader("device_id", "ditto:thing") + .acknowledgementRequest(AcknowledgementRequest.of(acknowledgementLabel)) + .build(); + final ThingEvent source = + ThingMerged.of(TestConstants.Things.THING_ID, JsonPointer.empty(), JsonObject.newBuilder() + .set("attributes", JsonObject.newBuilder() + .set("two", 2) + .build() + ) + .build(), + 4L, + Instant.now(), + dittoHeaders, + null); + final OutboundMappingProcessorActor.OutboundSignalWithSender outboundSignal = + OutboundMappingProcessorActor.OutboundSignalWithSender.of(source, null) + .setExtra(KNOWN_THING.toJson()); + final ExternalMessage externalMessage = ExternalMessageFactory.newExternalMessageBuilder(Map.of()) + .withText("payload") + .build(); + final Adaptable adaptable = DittoProtocolAdapter.newInstance().toAdaptable(source); + final OutboundSignal.Mapped mappedSignal = + OutboundSignalFactory.newMappedOutboundSignal(outboundSignal, adaptable, externalMessage); + + final ExpressionResolver expressionResolver = Resolvers.forOutbound(mappedSignal, CONNECTION_ID); + final Optional resolvedOptional = + expressionResolver.resolve("{{ thing-json:attributes/one }}").findFirst(); + + assertThat(resolvedOptional).contains("1"); + } } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPublisherActorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPublisherActorTest.java index 4040777d42..e564c99c8f 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPublisherActorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPublisherActorTest.java @@ -58,6 +58,7 @@ import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.messages.model.Message; import org.eclipse.ditto.messages.model.MessageDirection; @@ -67,6 +68,8 @@ import org.eclipse.ditto.messages.model.signals.commands.SendThingMessageResponse; import org.eclipse.ditto.protocol.ProtocolFactory; import org.eclipse.ditto.protocol.adapter.DittoProtocolAdapter; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingFieldSelector; import org.eclipse.ditto.things.model.ThingId; import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing; import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse; @@ -859,6 +862,64 @@ public void testAwsRequestSigning() throws Exception { }}; } + @Test + public void testSendMessageUsingThingJsonPlaceholderOnExtraAttributes() throws Exception { + new TestKit(actorSystem) {{ + httpPushFactory = mockHttpPushFactory("application/json", HttpStatus.OK, ""); + final var target = ConnectivityModelFactory.newTargetBuilder() + .address("POST:/foo/{{ thing-json:attributes/location }}") + .authorizationContext(TestConstants.Authorization.AUTHORIZATION_CONTEXT) + .topics(ConnectivityModelFactory.newFilteredTopicBuilder(Topic.LIVE_MESSAGES) + .withExtraFields(ThingFieldSelector.fromString("attributes")) + .build()) + .build(); + + final var connection = TestConstants.createConnection() + .toBuilder() + .targets(List.of(target)) + .build(); + final var props = HttpPublisherActor.props(connection, + httpPushFactory, + mock(ConnectivityStatusResolver.class), + ConnectivityConfig.of(actorSystem.settings().config())); + final var publisherActor = childActorOf(props); + publisherCreated(this, publisherActor); + + // WHEN: HTTP publisher sends an HTTP request + final Message message = Message.newBuilder( + MessageHeaders.newBuilder(MessageDirection.TO, TestConstants.Things.THING_ID, "fetch-weather") + .build() + ).build(); + final Signal source = SendThingMessage.of(TestConstants.Things.THING_ID, message, DittoHeaders.empty()); + final var outboundSignal = + OutboundSignalFactory.newOutboundSignal(source, Collections.singletonList(target)); + final var externalMessage = + ExternalMessageFactory.newExternalMessageBuilder(Collections.emptyMap()) + .withText("payload") + .build(); + var adaptable = DittoProtocolAdapter.newInstance().toAdaptable(source); + adaptable = ProtocolFactory.setExtra( + adaptable, + Thing.newBuilder() + .setAttribute(JsonPointer.of("location"), JsonValue.of("Hamburg")) + .build() + .toJson() + ); + final var mapped = + OutboundSignalFactory.newMappedOutboundSignal(outboundSignal, adaptable, externalMessage); + + publisherActor.tell( + OutboundSignalFactory.newMultiMappedOutboundSignal(Collections.singletonList(mapped), getRef()), + getRef()); + + // THEN: The request is signed by the configured request signing process. + final var request = received.take(); + assertThat(received).isEmpty(); + assertThat(request.method()).isEqualTo(HttpMethods.POST); + assertThat(request.getUri().getPathString()).isEqualTo("/foo/Hamburg"); + }}; + } + @Test public void testReservedHeaders() throws Exception { // GIVEN: reserved headers are set diff --git a/documentation/src/main/resources/pages/ditto/basic-placeholders.md b/documentation/src/main/resources/pages/ditto/basic-placeholders.md index 43c45db45a..b2448ffc58 100644 --- a/documentation/src/main/resources/pages/ditto/basic-placeholders.md +++ b/documentation/src/main/resources/pages/ditto/basic-placeholders.md @@ -27,6 +27,12 @@ Which placeholder values are available depends on the context where the placehol | `{%raw%}{{ thing:namespace }}{%endraw%}` | the namespace (i.e. first part of an ID) of the related thing | | `{%raw%}{{ thing:name }}{%endraw%}` | the name (i.e. second part of an ID ) of the related thing | +### Thing JSON Placeholder + +| Placeholder | Description | +|----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `{%raw%}{{ thing-json: }}{%endraw%}` | Value (in string representation) of the JSON identified by the provided ''json-pointer'' in JsonPointer notation - e.g., `thing-json:attributes/location` for the "location" attribute or `thing-json:features/temperature/properties/value` for the temperature property. | + ### Feature Placeholder | Placeholder | Description | @@ -188,7 +194,7 @@ When [declaring extra fields](basic-enrichment.html) which should be enriched to * [time placeholder](#time-placeholder) ### Scope: SSE Signal Enrichment -When [declaring extra fields](basic-enrichment.html) which should be enriched to a published signal for a Websocket connection, the following placeholders are available in general: +When [declaring extra fields](basic-enrichment.html) which should be enriched to a published signal for an SSE subscription, the following placeholders are available in general: * [entity placeholder](#entity-placeholder) * [thing placeholder](#thing-placeholder) * [policy placeholder](#policy-placeholder) @@ -204,6 +210,7 @@ When [declaring extra fields](basic-enrichment.html) which should be enriched to In [connections](basic-connections.html), the following placeholders are available in general: * [entity placeholder](#entity-placeholder) * [thing placeholder](#thing-placeholder) +* [thing-json placeholder](#thing-json-placeholder) * [policy placeholder](#policy-placeholder) * [connection placeholder](#connection-placeholder) * [feature placeholder](#feature-placeholder) @@ -268,17 +275,22 @@ be placeholders again. The following functions are provided by Ditto out of the box: -| Name | Signature | Description | Examples | -|-----------------------|------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| Name | Signature | Description | Examples | +|-----------------------|------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| | `fn:filter` | `(String filterValue, String rqlFunction, String comparedValue)` | Removes the result of the previous expression in the pipeline unless the condition specified by the parameters is satisfied.
The first parameter `filterValue` may also be omitted in which case the 2 passed parameters will be appplied on the previous pipeline expression. | `fn:filter('like','allowlist1|foobar|include')`
`fn:filter(header:response-required,'eq','true')`
`fn:filter(header:response-required,'exists')`
`fn:filter(header:response-required,'exists','false')` | -| `fn:default` | `(String defaultValue)` | Provides the passed `defaultValue` when the previous expression in a pipeline resolved to `empty` (e.g. due to a non-defined `header` placeholder key).
Another placeholder may be specified which is resolved to a String and inserted as `defaultValue`. | `fn:default('fallback')`
`fn:default("fallback")`
`fn:default(thing:id)` | -| `fn:substring-before` | `(String givenString)` | Parses the result of the previous expression and passes along only the characters _before_ the first occurrence of `givenString`.
If `givenString` is not contained, this function will resolve to `empty`. | `fn:substring-before(':')`
`fn:substring-before(":")` | -| `fn:substring-after` | `(String givenString)` | Parses the result of the previous expression and passes along only the characters _after_ the first occurrence of `givenString`.
If `givenString` is not contained, this function will resolve to `empty`. | `fn:substring-after(':')`
`fn:substring-after(":")` | -| `fn:lower` | `()` | Makes the String result of the previous expression lowercase in total. | `fn:lower()` | -| `fn:upper` | `()` | Makes the String result of the previous expression uppercase in total. | `fn:upper()` | -| `fn:delete` | `()` | Deletes the result of the previous pipeline expression unconditionally. Any following expressions are ignored. | `fn:delete()` | -| `fn:replace` | `(String from, String to)` | Replaces a string with another using Java's `String::replace` method. | `fn:replace('foo', 'bar')` | -| `fn:split` | `(String separator)` | Splits the previous pipeline using the passed `separator` resulting an "array" pipeline output containing several elements.
May only be used in combination with the [JWT placeholder](#scope-openid-connect-configuration) as input placeholder. | `fn:split(' ')`
`fn:split(',')` | +| `fn:default` | `(String defaultValue)` | Provides the passed `defaultValue` when the previous expression in a pipeline resolved to `empty` (e.g. due to a non-defined `header` placeholder key).
Another placeholder may be specified which is resolved to a String and inserted as `defaultValue`. | `fn:default('fallback')`
`fn:default("fallback")`
`fn:default(thing:id)` | +| `fn:substring-before` | `(String givenString)` | Parses the result of the previous expression and passes along only the characters _before_ the first occurrence of `givenString`.
If `givenString` is not contained, this function will resolve to `empty`. | `fn:substring-before(':')`
`fn:substring-before(":")` | +| `fn:substring-after` | `(String givenString)` | Parses the result of the previous expression and passes along only the characters _after_ the first occurrence of `givenString`.
If `givenString` is not contained, this function will resolve to `empty`. | `fn:substring-after(':')`
`fn:substring-after(":")` | +| `fn:lower` | `()` | Makes the String result of the previous expression lowercase in total. | `fn:lower()` | +| `fn:upper` | `()` | Makes the String result of the previous expression uppercase in total. | `fn:upper()` | +| `fn:trim` | `()` | Trims the String result of the previous expression, removing leading and trailing whitespaces. | `fn:trim()` | +| `fn:url-encode` | `()` | Applies URL encoding to the String result of the previous expression. | `fn:url-encode()` | +| `fn:url-decode` | `()` | Applies URL decoding to the String result of the previous expression. | `fn:url-decode()` | +| `fn:base64-encode` | `()` | Applies Base64 encoding to the String result of the previous expression. | `fn:base64-encode()` | +| `fn:base64-decode` | `()` | Applies Base64 decoding to the String result of the previous expression. | `fn:base64-decode()` | +| `fn:delete` | `()` | Deletes the result of the previous pipeline expression unconditionally. Any following expressions are ignored. | `fn:delete()` | +| `fn:replace` | `(String from, String to)` | Replaces a string with another using Java's `String::replace` method. | `fn:replace('foo', 'bar')` | +| `fn:split` | `(String separator)` | Splits the previous pipeline using the passed `separator` resulting an "array" pipeline output containing several elements.
May only be used in combination with the [JWT placeholder](#scope-openid-connect-configuration) as input placeholder. | `fn:split(' ')`
`fn:split(',')` | ### RQL functions diff --git a/edge/service/src/main/java/org/eclipse/ditto/edge/service/placeholders/ImmutableThingJsonPlaceholder.java b/edge/service/src/main/java/org/eclipse/ditto/edge/service/placeholders/ImmutableThingJsonPlaceholder.java new file mode 100644 index 0000000000..bd3baa33aa --- /dev/null +++ b/edge/service/src/main/java/org/eclipse/ditto/edge/service/placeholders/ImmutableThingJsonPlaceholder.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.edge.service.placeholders; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.argumentNotEmpty; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.base.model.json.FieldType; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Thing; + +/** + * Placeholder implementation that replaces {@code thing-json:attributes/some-attr} and other arbitrary json values inside + * a Thing. + */ +@Immutable +final class ImmutableThingJsonPlaceholder implements ThingJsonPlaceholder { + + /** + * Singleton instance of the ImmutableThingJsonPlaceholder. + */ + static final ImmutableThingJsonPlaceholder INSTANCE = new ImmutableThingJsonPlaceholder(); + + @Override + public String getPrefix() { + return "thing-json"; + } + + @Override + public List getSupportedNames() { + // supports any names (interpreted as JsonPointer) + return List.of(); + } + + @Override + public boolean supports(final String name) { + // supports any names (interpreted as JsonPointer) BUT the ones supported by ImmutableThingPlaceholder + return !List.of("namespace", "name", "id").contains(name); + } + + @Override + public List resolveValues(final Thing thing, final String placeholder) { + checkNotNull(thing, "thing"); + argumentNotEmpty(placeholder, "placeholder"); + final Optional value = thing.toJson(FieldType.all()).getValue(placeholder); + return value.filter(JsonValue::isArray) + .map(JsonValue::asArray) + .map(array -> array.stream() + .map(JsonValue::formatAsString) + .toList()) + .or(() -> value.map(JsonValue::formatAsString) + .map(Collections::singletonList)) + .orElseGet(Collections::emptyList); + } +} diff --git a/edge/service/src/main/java/org/eclipse/ditto/edge/service/placeholders/ThingJsonPlaceholder.java b/edge/service/src/main/java/org/eclipse/ditto/edge/service/placeholders/ThingJsonPlaceholder.java new file mode 100644 index 0000000000..6bbe87d943 --- /dev/null +++ b/edge/service/src/main/java/org/eclipse/ditto/edge/service/placeholders/ThingJsonPlaceholder.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.edge.service.placeholders; + +import org.eclipse.ditto.placeholders.Placeholder; +import org.eclipse.ditto.things.model.Thing; + +/** + * A {@link org.eclipse.ditto.placeholders.Placeholder} that requires a {@code Thing} + * to resolve content of the thing via JsonPointer notation and resolved e.g. {@code thing-json:attributes/some-attr} + */ +public interface ThingJsonPlaceholder extends Placeholder { + + static ThingJsonPlaceholder getInstance() { + return ImmutableThingJsonPlaceholder.INSTANCE; + } + +} diff --git a/edge/service/src/test/java/org/eclipse/ditto/edge/service/placeholders/ImmutableThingJsonPlaceholderTest.java b/edge/service/src/test/java/org/eclipse/ditto/edge/service/placeholders/ImmutableThingJsonPlaceholderTest.java new file mode 100644 index 0000000000..2735eec9b0 --- /dev/null +++ b/edge/service/src/test/java/org/eclipse/ditto/edge/service/placeholders/ImmutableThingJsonPlaceholderTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.edge.service.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Thing; +import org.junit.Test; +import org.mutabilitydetector.unittesting.MutabilityAssert; +import org.mutabilitydetector.unittesting.MutabilityMatchers; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; + +/** + * Tests {@link ImmutableThingJsonPlaceholder}. + */ +public class ImmutableThingJsonPlaceholderTest { + + private static final Thing KNOWN_THING = Thing.newBuilder() + .setAttributes( + JsonObject.newBuilder() + .set("int", 1) + .set("num", 2.3) + .set("bool", true) + .set("obj", JsonObject.newBuilder() + .set("str", "bar") + .set("arr", JsonArray.newBuilder() + .add(1, 2, 3) + .build()) + .set("evenDeeper", JsonObject.newBuilder() + .set("str", "bar2") + .build()) + .build()) + .build() + ) + .setFeature("some-feature", FeatureProperties.newBuilder() + .set("some-prop", "abc") + .build() + ) + .build(); + private static final ThingJsonPlaceholder UNDER_TEST = ImmutableThingJsonPlaceholder.INSTANCE; + + @Test + public void assertImmutability() { + MutabilityAssert.assertInstancesOf(ImmutableThingJsonPlaceholder.class, MutabilityMatchers.areImmutable()); + } + + @Test + public void testHashCodeAndEquals() { + EqualsVerifier.forClass(ImmutableThingJsonPlaceholder.class) + .suppress(Warning.INHERITED_DIRECTLY_FROM_OBJECT) + .usingGetClass() + .verify(); + } + + @Test + public void testReplaceIntegerAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/int")).contains(String.valueOf(1)); + } + + @Test + public void testReplaceNumberAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/num")).contains(String.valueOf(2.3)); + } + + @Test + public void testReplaceBooleanAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/bool")).contains(String.valueOf(true)); + } + + @Test + public void testReplaceNestedStrAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/obj/str")).contains("bar"); + } + + @Test + public void testReplaceNestedArrAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/obj/arr")) + .containsExactly(String.valueOf(1), String.valueOf(2), String.valueOf(3)); + } + + @Test + public void testReplaceObjAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/obj/evenDeeper")).contains("{\"str\":\"bar2\"}"); + } + + @Test + public void testReplaceDeeperNestedStrAttribute() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "attributes/obj/evenDeeper/str")).contains("bar2"); + } + + @Test + public void testReplaceStrFeatureProperty() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "features/some-feature/properties/some-prop")) + .contains("abc"); + } + + @Test + public void testUnknownPlaceholderReturnsEmpty() { + assertThat(UNDER_TEST.resolveValues(KNOWN_THING, "unknown")).isEmpty(); + } + + @Test + public void testResolvingWithNull() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> UNDER_TEST.resolveValues(KNOWN_THING, null)); + } + + @Test + public void testResolvingWithEmptyString() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> UNDER_TEST.resolveValues(KNOWN_THING, "")); + } + +} diff --git a/placeholders/src/main/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpression.java b/placeholders/src/main/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpression.java index ab8b6dd8be..5b0d823033 100644 --- a/placeholders/src/main/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpression.java +++ b/placeholders/src/main/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpression.java @@ -37,6 +37,11 @@ final class ImmutableFunctionExpression implements FunctionExpression { new PipelineFunctionSubstringAfter(), // fn:substring-after(':') new PipelineFunctionLower(), // fn:lower() new PipelineFunctionUpper(), // fn:upper() + new PipelineFunctionTrim(), // fn:trim() + new PipelineFunctionUrlEncode(), // fn:url-encode() + new PipelineFunctionUrlDecode(), // fn:url-decode() + new PipelineFunctionBase64Encode(), // fn:base64-encode() + new PipelineFunctionBase64Decode(), // fn:base64-decode() new PipelineFunctionDelete(), // fn:delete() new PipelineFunctionReplace(), // fn:replace('from', 'to') new PipelineFunctionSplit() // fn:split(' ') diff --git a/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64Decode.java b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64Decode.java new file mode 100644 index 0000000000..4d18bc147c --- /dev/null +++ b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64Decode.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +import javax.annotation.concurrent.Immutable; + +/** + * Provides the {@code fn:base64-decode()} function implementation. + */ +@Immutable +final class PipelineFunctionBase64Decode implements PipelineFunction { + + private static final String FUNCTION_NAME = "base64-decode"; + + @Override + public String getName() { + return FUNCTION_NAME; + } + + @Override + public Base64DecodeSignature getSignature() { + return Base64DecodeSignature.INSTANCE; + } + + @Override + public PipelineElement apply(final PipelineElement value, final String paramsIncludingParentheses, + final ExpressionResolver expressionResolver) { + + // check if signature matches (empty params!) + validateOrThrow(paramsIncludingParentheses); + return value.map(v -> new String(Base64.getDecoder().decode(v))); + } + + private void validateOrThrow(final String paramsIncludingParentheses) { + if (!PipelineFunctionParameterResolverFactory.forEmptyParameters().test(paramsIncludingParentheses)) { + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); + } + } + + /** + * Describes the signature of the {@code base64-decode()} function. + */ + private static final class Base64DecodeSignature implements Signature { + + private static final Base64DecodeSignature INSTANCE = new Base64DecodeSignature(); + + private Base64DecodeSignature() { + } + + @Override + public List> getParameterDefinitions() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return renderSignature(); + } + } + +} diff --git a/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64Encode.java b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64Encode.java new file mode 100644 index 0000000000..8e4eaaf438 --- /dev/null +++ b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64Encode.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +import javax.annotation.concurrent.Immutable; + +/** + * Provides the {@code fn:base64-encode()} function implementation. + */ +@Immutable +final class PipelineFunctionBase64Encode implements PipelineFunction { + + private static final String FUNCTION_NAME = "base64-encode"; + + @Override + public String getName() { + return FUNCTION_NAME; + } + + @Override + public Base64EncodeSignature getSignature() { + return Base64EncodeSignature.INSTANCE; + } + + @Override + public PipelineElement apply(final PipelineElement value, final String paramsIncludingParentheses, + final ExpressionResolver expressionResolver) { + + // check if signature matches (empty params!) + validateOrThrow(paramsIncludingParentheses); + return value.map(v -> Base64.getEncoder().encodeToString(v.getBytes(StandardCharsets.UTF_8))); + } + + private void validateOrThrow(final String paramsIncludingParentheses) { + if (!PipelineFunctionParameterResolverFactory.forEmptyParameters().test(paramsIncludingParentheses)) { + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); + } + } + + /** + * Describes the signature of the {@code base64-encode()} function. + */ + private static final class Base64EncodeSignature implements Signature { + + private static final Base64EncodeSignature INSTANCE = new Base64EncodeSignature(); + + private Base64EncodeSignature() { + } + + @Override + public List> getParameterDefinitions() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return renderSignature(); + } + } + +} diff --git a/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionTrim.java b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionTrim.java new file mode 100644 index 0000000000..054072d72b --- /dev/null +++ b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionTrim.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import java.util.Collections; +import java.util.List; + +import javax.annotation.concurrent.Immutable; + +/** + * Provides the {@code fn:trim()} function implementation. + */ +@Immutable +final class PipelineFunctionTrim implements PipelineFunction { + + private static final String FUNCTION_NAME = "trim"; + + @Override + public String getName() { + return FUNCTION_NAME; + } + + @Override + public TrimFunctionSignature getSignature() { + return TrimFunctionSignature.INSTANCE; + } + + @Override + public PipelineElement apply(final PipelineElement value, final String paramsIncludingParentheses, + final ExpressionResolver expressionResolver) { + + // check if signature matches (empty params!) + validateOrThrow(paramsIncludingParentheses); + return value.map(String::trim); + } + + private void validateOrThrow(final String paramsIncludingParentheses) { + if (!PipelineFunctionParameterResolverFactory.forEmptyParameters().test(paramsIncludingParentheses)) { + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); + } + } + + /** + * Describes the signature of the {@code trim()} function. + */ + private static final class TrimFunctionSignature implements Signature { + + private static final TrimFunctionSignature INSTANCE = new TrimFunctionSignature(); + + private TrimFunctionSignature() { + } + + @Override + public List> getParameterDefinitions() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return renderSignature(); + } + } + +} diff --git a/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlDecode.java b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlDecode.java new file mode 100644 index 0000000000..c74b9017c9 --- /dev/null +++ b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlDecode.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import javax.annotation.concurrent.Immutable; + +/** + * Provides the {@code fn:url-decode()} function implementation. + */ +@Immutable +final class PipelineFunctionUrlDecode implements PipelineFunction { + + private static final String FUNCTION_NAME = "url-decode"; + + @Override + public String getName() { + return FUNCTION_NAME; + } + + @Override + public UrlDecodeSignature getSignature() { + return UrlDecodeSignature.INSTANCE; + } + + @Override + public PipelineElement apply(final PipelineElement value, final String paramsIncludingParentheses, + final ExpressionResolver expressionResolver) { + + // check if signature matches (empty params!) + validateOrThrow(paramsIncludingParentheses); + return value.map(v -> { + try { + return URLDecoder.decode(v, StandardCharsets.UTF_8.name()); + } catch (final UnsupportedEncodingException e) { + return URLDecoder.decode(v); + } + }); + } + + private void validateOrThrow(final String paramsIncludingParentheses) { + if (!PipelineFunctionParameterResolverFactory.forEmptyParameters().test(paramsIncludingParentheses)) { + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); + } + } + + /** + * Describes the signature of the {@code url-decode()} function. + */ + private static final class UrlDecodeSignature implements Signature { + + private static final UrlDecodeSignature INSTANCE = new UrlDecodeSignature(); + + private UrlDecodeSignature() { + } + + @Override + public List> getParameterDefinitions() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return renderSignature(); + } + } + +} diff --git a/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlEncode.java b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlEncode.java new file mode 100644 index 0000000000..f089906943 --- /dev/null +++ b/placeholders/src/main/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlEncode.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import javax.annotation.concurrent.Immutable; + +/** + * Provides the {@code fn:url-encode()} function implementation. + */ +@Immutable +final class PipelineFunctionUrlEncode implements PipelineFunction { + + private static final String FUNCTION_NAME = "url-encode"; + + @Override + public String getName() { + return FUNCTION_NAME; + } + + @Override + public UrlEncodeSignature getSignature() { + return UrlEncodeSignature.INSTANCE; + } + + @Override + public PipelineElement apply(final PipelineElement value, final String paramsIncludingParentheses, + final ExpressionResolver expressionResolver) { + + // check if signature matches (empty params!) + validateOrThrow(paramsIncludingParentheses); + return value.map(v -> { + try { + return URLEncoder.encode(v, StandardCharsets.UTF_8.name()); + } catch (final UnsupportedEncodingException e) { + return URLEncoder.encode(v); + } + }); + } + + private void validateOrThrow(final String paramsIncludingParentheses) { + if (!PipelineFunctionParameterResolverFactory.forEmptyParameters().test(paramsIncludingParentheses)) { + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); + } + } + + /** + * Describes the signature of the {@code url-encode()} function. + */ + private static final class UrlEncodeSignature implements Signature { + + private static final UrlEncodeSignature INSTANCE = new UrlEncodeSignature(); + + private UrlEncodeSignature() { + } + + @Override + public List> getParameterDefinitions() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return renderSignature(); + } + } + +} diff --git a/placeholders/src/test/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpressionTest.java b/placeholders/src/test/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpressionTest.java index 8fb326a2ff..185c6df315 100644 --- a/placeholders/src/test/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpressionTest.java +++ b/placeholders/src/test/java/org/eclipse/ditto/placeholders/ImmutableFunctionExpressionTest.java @@ -40,6 +40,11 @@ public class ImmutableFunctionExpressionTest { "substring-after", "lower", "upper", + "trim", + "url-encode", + "url-decode", + "base64-encode", + "base64-decode", "delete", "replace", "split" diff --git a/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64DecodeTest.java b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64DecodeTest.java new file mode 100644 index 0000000000..d9850de64d --- /dev/null +++ b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64DecodeTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PipelineFunctionBase64DecodeTest { + + private static final PipelineElement KNOWN_INPUT = PipelineElement.resolved("ICAgXyAgIF8gICBfICAgXyAgIF8gIAogIC8gXCAvIFwgLyBcIC8gXCAvIFwgCiAoIEQgfCBJIHwgVCB8IFQgfCBPICkKICBcXy8gXF8vIFxfLyBcXy8gXF8vIA=="); + private static final String ACTUAL_VALUE = + " _ _ _ _ _ \n" + + " / \\ / \\ / \\ / \\ / \\ \n" + + " ( D | I | T | T | O )\n" + + " \\_/ \\_/ \\_/ \\_/ \\_/ "; + + private final PipelineFunctionBase64Decode function = new PipelineFunctionBase64Decode(); + + @Mock + private ExpressionResolver expressionResolver; + + @After + public void verifyExpressionResolverUnused() { + Mockito.verifyNoInteractions(expressionResolver); + } + + @Test + public void getName() { + assertThat(function.getName()).isEqualTo("base64-decode"); + } + + @Test + public void apply() { + assertThat(function.apply(KNOWN_INPUT, "()", expressionResolver)).contains(ACTUAL_VALUE); + } + + @Test + public void throwsOnNonZeroParameters() { + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\"string\")", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\'string\')", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(thing:id)", expressionResolver)); + } + +} diff --git a/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64EncodeTest.java b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64EncodeTest.java new file mode 100644 index 0000000000..0075c918db --- /dev/null +++ b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionBase64EncodeTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PipelineFunctionBase64EncodeTest { + + private static final PipelineElement KNOWN_INPUT = PipelineElement.resolved( + " _ _ _ _ _ \n" + + " / \\ / \\ / \\ / \\ / \\ \n" + + " ( D | I | T | T | O )\n" + + " \\_/ \\_/ \\_/ \\_/ \\_/ "); + private static final String ACTUAL_VALUE = "ICAgXyAgIF8gICBfICAgXyAgIF8gIAogIC8gXCAvIFwgLyBcIC8gXCAvIFwgCiAoIEQgfCBJIHwgVCB8IFQgfCBPICkKICBcXy8gXF8vIFxfLyBcXy8gXF8vIA=="; + + private final PipelineFunctionBase64Encode function = new PipelineFunctionBase64Encode(); + + @Mock + private ExpressionResolver expressionResolver; + + @After + public void verifyExpressionResolverUnused() { + Mockito.verifyNoInteractions(expressionResolver); + } + + @Test + public void getName() { + assertThat(function.getName()).isEqualTo("base64-encode"); + } + + @Test + public void apply() { + assertThat(function.apply(KNOWN_INPUT, "()", expressionResolver)).contains(ACTUAL_VALUE); + } + + @Test + public void throwsOnNonZeroParameters() { + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\"string\")", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\'string\')", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(thing:id)", expressionResolver)); + } + +} diff --git a/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionTrimTest.java b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionTrimTest.java new file mode 100644 index 0000000000..73b3dd244a --- /dev/null +++ b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionTrimTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PipelineFunctionTrimTest { + + private static final PipelineElement KNOWN_INPUT_PREFIX = PipelineElement.resolved(" foo"); + private static final PipelineElement KNOWN_INPUT_SUFFIX = PipelineElement.resolved("foo "); + private static final PipelineElement KNOWN_INPUT_BOTH = PipelineElement.resolved(" foo "); + private static final String TRIMMED = "foo"; + + private final PipelineFunctionTrim function = new PipelineFunctionTrim(); + + @Mock + private ExpressionResolver expressionResolver; + + @After + public void verifyExpressionResolverUnused() { + Mockito.verifyNoInteractions(expressionResolver); + } + + @Test + public void getName() { + assertThat(function.getName()).isEqualTo("trim"); + } + + @Test + public void apply() { + assertThat(function.apply(KNOWN_INPUT_PREFIX, "()", expressionResolver)).contains(TRIMMED); + assertThat(function.apply(KNOWN_INPUT_SUFFIX, "()", expressionResolver)).contains(TRIMMED); + assertThat(function.apply(KNOWN_INPUT_BOTH, "()", expressionResolver)).contains(TRIMMED); + } + + @Test + public void throwsOnNonZeroParameters() { + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT_PREFIX, "", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT_PREFIX, "(\"string\")", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT_PREFIX, "(\'string\')", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT_PREFIX, "(thing:id)", expressionResolver)); + } + +} diff --git a/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlDecodeTest.java b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlDecodeTest.java new file mode 100644 index 0000000000..f66109f63e --- /dev/null +++ b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlDecodeTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PipelineFunctionUrlDecodeTest { + + private static final PipelineElement KNOWN_INPUT = PipelineElement.resolved("%22Hello%20World%2C%20from%20%C3%9Cberlingen%21%22%20%2F%20%2B%20%3B%20.%20foo%2C"); + private static final String ACTUAL_VALUE = "\"Hello World, from Überlingen!\" / + ; . foo,"; + + private final PipelineFunctionUrlDecode function = new PipelineFunctionUrlDecode(); + + @Mock + private ExpressionResolver expressionResolver; + + @After + public void verifyExpressionResolverUnused() { + Mockito.verifyNoInteractions(expressionResolver); + } + + @Test + public void getName() { + assertThat(function.getName()).isEqualTo("url-decode"); + } + + @Test + public void apply() { + assertThat(function.apply(KNOWN_INPUT, "()", expressionResolver)).contains(ACTUAL_VALUE); + } + + @Test + public void throwsOnNonZeroParameters() { + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\"string\")", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\'string\')", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(thing:id)", expressionResolver)); + } + +} diff --git a/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlEncodeTest.java b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlEncodeTest.java new file mode 100644 index 0000000000..6d28a0185a --- /dev/null +++ b/placeholders/src/test/java/org/eclipse/ditto/placeholders/PipelineFunctionUrlEncodeTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PipelineFunctionUrlEncodeTest { + + private static final PipelineElement KNOWN_INPUT = PipelineElement.resolved("\"Hello World, from Überlingen!\" / + ; . foo,"); + private static final String ACTUAL_VALUE = "%22Hello+World%2C+from+%C3%9Cberlingen%21%22+%2F+%2B+%3B+.+foo%2C"; + + private final PipelineFunctionUrlEncode function = new PipelineFunctionUrlEncode(); + + @Mock + private ExpressionResolver expressionResolver; + + @After + public void verifyExpressionResolverUnused() { + Mockito.verifyNoInteractions(expressionResolver); + } + + @Test + public void getName() { + assertThat(function.getName()).isEqualTo("url-encode"); + } + + @Test + public void apply() { + assertThat(function.apply(KNOWN_INPUT, "()", expressionResolver)).contains(ACTUAL_VALUE); + } + + @Test + public void throwsOnNonZeroParameters() { + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\"string\")", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(\'string\')", expressionResolver)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> function.apply(KNOWN_INPUT, "(thing:id)", expressionResolver)); + } + +} diff --git a/things/model/src/main/java/org/eclipse/ditto/things/model/signals/events/ThingEventToThingConverter.java b/things/model/src/main/java/org/eclipse/ditto/things/model/signals/events/ThingEventToThingConverter.java index 8b05962fd2..2c6d28be3e 100644 --- a/things/model/src/main/java/org/eclipse/ditto/things/model/signals/events/ThingEventToThingConverter.java +++ b/things/model/src/main/java/org/eclipse/ditto/things/model/signals/events/ThingEventToThingConverter.java @@ -76,7 +76,7 @@ public static Optional thingEventToThing(final ThingEvent thingEvent) * @param extra value of the extra fields. * @return the merged thing if thing information exists in any of the 2 sources, or an empty optional otherwise. */ - public static Optional mergeThingWithExtraFields(final Signal signal, + public static Optional mergeThingWithExtraFields(@Nullable final Signal signal, @Nullable final JsonFieldSelector extraFields, final JsonObject extra) {