diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcerActor.java b/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcerActor.java index cf59a75149..a494d74344 100644 --- a/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcerActor.java +++ b/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcerActor.java @@ -19,6 +19,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Function; +import java.util.function.Supplier; import javax.annotation.Nullable; @@ -93,6 +94,7 @@ import org.eclipse.ditto.things.model.signals.commands.modify.ThingModifyCommand; import org.eclipse.ditto.wot.api.validator.WotThingModelValidator; import org.eclipse.ditto.wot.integration.DittoWotIntegration; +import org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException; /** * Enforcer responsible for enforcing {@link ThingCommand}s and filtering {@link ThingCommandResponse}s utilizing the @@ -572,29 +574,36 @@ private CompletionStage doesThingExist() { private CompletionStage> performWotBasedMessageCommandValidation( final MessageCommand messageCommand ) { - if (isJsonMessageContent(messageCommand.getMessage())) { - @SuppressWarnings("unchecked") final Message message = - ((MessageCommand) messageCommand) - .getMessage(); - - final MessageDirection messageDirection = message.getDirection(); - final JsonValue messageCommandPayload = message - .getPayload() - .orElse(null); - - if (messageCommand instanceof SendThingMessage sendThingMessage) { - return performWotBasedThingMessageValidation(messageCommand, sendThingMessage, messageDirection, - messageCommandPayload - ).thenApply(aVoid -> messageCommand); - } else if (messageCommand instanceof SendFeatureMessage sendFeatureMessage) { - final String featureId = sendFeatureMessage.getFeatureId(); - return performWotBasedFeatureMessageValidation(messageCommand, sendFeatureMessage, featureId, - messageDirection, messageCommandPayload - ).thenApply(aVoid -> messageCommand); - - } else { - return CompletableFuture.completedFuture(messageCommand); + @SuppressWarnings("unchecked") final Message message = + ((MessageCommand) messageCommand) + .getMessage(); + + // lazily only supply JsonValue if validation is enabled for the message: + final Supplier messageCommandPayloadSupplier = () -> { + if (message.getPayload().isPresent() && !isJsonMessageContent(message)) { + throw WotThingModelPayloadValidationException + .newBuilder("Could not validate non-JSON message content type <" + + message.getContentType().orElse("?") + "> for message subject " + + "<" + message.getSubject() + ">" + ) + .dittoHeaders(messageCommand.getDittoHeaders()) + .build(); } + + return message.getPayload().orElse(null); + }; + + final MessageDirection messageDirection = message.getDirection(); + if (messageCommand instanceof SendThingMessage sendThingMessage) { + return performWotBasedThingMessageValidation(messageCommand, sendThingMessage, messageDirection, + messageCommandPayloadSupplier + ).thenApply(aVoid -> messageCommand); + } else if (messageCommand instanceof SendFeatureMessage sendFeatureMessage) { + final String featureId = sendFeatureMessage.getFeatureId(); + return performWotBasedFeatureMessageValidation(messageCommand, sendFeatureMessage, featureId, + messageDirection, messageCommandPayloadSupplier + ).thenApply(aVoid -> messageCommand); + } else { return CompletableFuture.completedFuture(messageCommand); } @@ -603,7 +612,7 @@ private CompletionStage doesThingExist() { private CompletionStage performWotBasedThingMessageValidation(final MessageCommand messageCommand, final SendThingMessage sendThingMessage, final MessageDirection messageDirection, - @Nullable final JsonValue messageCommandPayload + final Supplier messageCommandPayloadSupplier ) { return resolveThingDefinition() .thenCompose(optThingDefinition -> { @@ -611,7 +620,7 @@ private CompletionStage performWotBasedThingMessageValidation(final Messag return thingModelValidator.validateThingActionInput( optThingDefinition.orElse(null), sendThingMessage.getMessage().getSubject(), - messageCommandPayload, + messageCommandPayloadSupplier, sendThingMessage.getResourcePath(), sendThingMessage.getDittoHeaders() ); @@ -619,7 +628,7 @@ private CompletionStage performWotBasedThingMessageValidation(final Messag return thingModelValidator.validateThingEventData( optThingDefinition.orElse(null), sendThingMessage.getMessage().getSubject(), - messageCommandPayload, + messageCommandPayloadSupplier, sendThingMessage.getResourcePath(), sendThingMessage.getDittoHeaders() ); @@ -637,7 +646,7 @@ private CompletionStage performWotBasedFeatureMessageValidation(final Mess final SendFeatureMessage sendFeatureMessage, final String featureId, final MessageDirection messageDirection, - @Nullable final JsonValue messageCommandPayload + final Supplier messageCommandPayloadSupplier ) { return resolveThingAndFeatureDefinition(featureId) .thenCompose(optDefinitionPair -> { @@ -647,7 +656,7 @@ private CompletionStage performWotBasedFeatureMessageValidation(final Mess optDefinitionPair.second().orElse(null), featureId, sendFeatureMessage.getMessage().getSubject(), - messageCommandPayload, + messageCommandPayloadSupplier, sendFeatureMessage.getResourcePath(), sendFeatureMessage.getDittoHeaders() ); @@ -657,7 +666,7 @@ private CompletionStage performWotBasedFeatureMessageValidation(final Mess optDefinitionPair.second().orElse(null), featureId, sendFeatureMessage.getMessage().getSubject(), - messageCommandPayload, + messageCommandPayloadSupplier, sendFeatureMessage.getResourcePath(), sendFeatureMessage.getDittoHeaders() ); @@ -674,44 +683,63 @@ private CompletionStage performWotBasedFeatureMessageValidation(final Mess private CompletionStage> performWotBasedMessageCommandResponseValidation( final MessageCommandResponse messageCommandResponse ) { - if (isJsonMessageContent(messageCommandResponse.getMessage())) { - @SuppressWarnings("unchecked") final Message message = - ((MessageCommandResponse) messageCommandResponse) - .getMessage(); - - final MessageDirection messageDirection = message.getDirection(); - final JsonValue messageCommandPayload = message - .getPayload() - .orElse(null); - - if (messageDirection == MessageDirection.TO && - messageCommandResponse instanceof SendThingMessageResponse sendThingMessageResponse) { - return resolveThingDefinition() - .thenCompose(optThingDefinition -> thingModelValidator.validateThingActionOutput( - optThingDefinition.orElse(null), - sendThingMessageResponse.getMessage().getSubject(), - messageCommandPayload, - sendThingMessageResponse.getResourcePath(), - sendThingMessageResponse.getDittoHeaders() - )) - .thenApply(aVoid -> messageCommandResponse); - } else if (messageDirection == MessageDirection.TO && - messageCommandResponse instanceof SendFeatureMessageResponse sendFeatureMessageResponse) { - final String featureId = sendFeatureMessageResponse.getFeatureId(); - return resolveThingAndFeatureDefinition(featureId) - .thenCompose(optDefinitionPair -> thingModelValidator.validateFeatureActionOutput( - optDefinitionPair.first().orElse(null), - optDefinitionPair.second().orElse(null), - featureId, - sendFeatureMessageResponse.getMessage().getSubject(), - messageCommandPayload, - sendFeatureMessageResponse.getResourcePath(), - sendFeatureMessageResponse.getDittoHeaders() - )) - .thenApply(aVoid -> messageCommandResponse); - } else { - return CompletableFuture.completedFuture(messageCommandResponse); + @SuppressWarnings("unchecked") final Message message = + ((MessageCommandResponse) messageCommandResponse) + .getMessage(); + + if (message.getPayload().isPresent() && !isJsonMessageContent(message)) { + return CompletableFuture.failedFuture( + WotThingModelPayloadValidationException + .newBuilder("Could not validate non-JSON message content type <" + + message.getContentType().orElse("?") + "> for message response subject " + + "<" + message.getSubject() + ">" + ) + .dittoHeaders(messageCommandResponse.getDittoHeaders()) + .build() + ); + } + + // lazily only supply JsonValue if validation is enabled for the message: + final Supplier messageCommandPayloadSupplier = () -> { + if (message.getPayload().isPresent() && !isJsonMessageContent(message)) { + throw WotThingModelPayloadValidationException + .newBuilder("Could not validate non-JSON message content type <" + + message.getContentType().orElse("?") + "> for message response subject " + + "<" + message.getSubject() + ">" + ) + .dittoHeaders(messageCommandResponse.getDittoHeaders()) + .build(); } + + return message.getPayload().orElse(null); + }; + + final MessageDirection messageDirection = message.getDirection(); + if (messageDirection == MessageDirection.TO && + messageCommandResponse instanceof SendThingMessageResponse sendThingMessageResponse) { + return resolveThingDefinition() + .thenCompose(optThingDefinition -> thingModelValidator.validateThingActionOutput( + optThingDefinition.orElse(null), + sendThingMessageResponse.getMessage().getSubject(), + messageCommandPayloadSupplier, + sendThingMessageResponse.getResourcePath(), + sendThingMessageResponse.getDittoHeaders() + )) + .thenApply(aVoid -> messageCommandResponse); + } else if (messageDirection == MessageDirection.TO && + messageCommandResponse instanceof SendFeatureMessageResponse sendFeatureMessageResponse) { + final String featureId = sendFeatureMessageResponse.getFeatureId(); + return resolveThingAndFeatureDefinition(featureId) + .thenCompose(optDefinitionPair -> thingModelValidator.validateFeatureActionOutput( + optDefinitionPair.first().orElse(null), + optDefinitionPair.second().orElse(null), + featureId, + sendFeatureMessageResponse.getMessage().getSubject(), + messageCommandPayloadSupplier, + sendFeatureMessageResponse.getResourcePath(), + sendFeatureMessageResponse.getDittoHeaders() + )) + .thenApply(aVoid -> messageCommandResponse); } else { return CompletableFuture.completedFuture(messageCommandResponse); } diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/enforcement/LiveSignalEnforcementTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/enforcement/LiveSignalEnforcementTest.java index fa8fbf50f3..c0662c11bf 100644 --- a/things/service/src/test/java/org/eclipse/ditto/things/service/enforcement/LiveSignalEnforcementTest.java +++ b/things/service/src/test/java/org/eclipse/ditto/things/service/enforcement/LiveSignalEnforcementTest.java @@ -305,6 +305,7 @@ public void correlationIdDifferentInCaseOfConflict() { supervisor.tell(message, getRef()); + expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); final var firstPublishRead = expectPubsubMessagePublish(message.getEntityId()); @@ -315,6 +316,7 @@ public void correlationIdDifferentInCaseOfConflict() { supervisor.tell(message, getRef()); + expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); final var secondPublishRead = expectPubsubMessagePublish(message.getEntityId()); @@ -351,6 +353,7 @@ public void acceptMessageCommandByPolicy() { final MessageCommand msgCommand = thingMessageCommand("abc"); supervisor.tell(msgCommand, getRef()); + expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); expectPubsubMessagePublish(msgCommand.getEntityId()); @@ -378,6 +381,7 @@ public void acceptFeatureMessageCommandByPolicy() { final MessageCommand msgCommand = featureMessageCommand(); supervisor.tell(msgCommand, getRef()); + expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); expectAndAnswerSudoRetrieveThing(sudoRetrieveThingResponse); expectPubsubMessagePublish(msgCommand.getEntityId()); diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java index debea34997..3a736a7b25 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java @@ -24,6 +24,7 @@ import java.util.concurrent.Executor; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -231,7 +232,7 @@ public CompletionStage validateThingScopedDeletion(@Nullable final ThingDe @Override public CompletionStage validateThingActionInput(@Nullable final ThingDefinition thingDefinition, final String messageSubject, - @Nullable final JsonValue inputPayload, + final Supplier inputPayloadSupplier, final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { @@ -240,7 +241,7 @@ public CompletionStage validateThingActionInput(@Nullable final ThingDefin .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> selectValidation(validationConfig) .validateThingActionInput(thingModel, - messageSubject, inputPayload, resourcePath, context + messageSubject, inputPayloadSupplier.get(), resourcePath, context ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingActionInput")) )) .orElseGet(DefaultWotThingModelValidator::success); @@ -249,7 +250,7 @@ public CompletionStage validateThingActionInput(@Nullable final ThingDefin @Override public CompletionStage validateThingActionOutput(@Nullable final ThingDefinition thingDefinition, final String messageSubject, - @Nullable final JsonValue outputPayload, + final Supplier outputPayloadSupplier, final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { @@ -258,7 +259,7 @@ public CompletionStage validateThingActionOutput(@Nullable final ThingDefi .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> selectValidation(validationConfig) .validateThingActionOutput(thingModel, - messageSubject, outputPayload, resourcePath, context + messageSubject, outputPayloadSupplier.get(), resourcePath, context ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingActionOutput")) )) .orElseGet(DefaultWotThingModelValidator::success); @@ -267,7 +268,7 @@ public CompletionStage validateThingActionOutput(@Nullable final ThingDefi @Override public CompletionStage validateThingEventData(@Nullable final ThingDefinition thingDefinition, final String messageSubject, - @Nullable final JsonValue dataPayload, + final Supplier dataPayloadSupplier, final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { @@ -276,7 +277,7 @@ public CompletionStage validateThingEventData(@Nullable final ThingDefinit .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> selectValidation(validationConfig) .validateThingEventData(thingModel, - messageSubject, dataPayload, resourcePath, context + messageSubject, dataPayloadSupplier.get(), resourcePath, context ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingEventData")) )) .orElseGet(DefaultWotThingModelValidator::success); @@ -493,7 +494,7 @@ public CompletionStage validateFeatureActionInput(@Nullable final ThingDef @Nullable final FeatureDefinition featureDefinition, final String featureId, final String messageSubject, - @Nullable final JsonValue inputPayload, + final Supplier inputPayloadSupplier, final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { @@ -504,7 +505,7 @@ public CompletionStage validateFeatureActionInput(@Nullable final ThingDef dittoHeaders, featureThingModel -> selectValidation(validationConfig) .validateFeatureActionInput(featureThingModel, - featureId, messageSubject, inputPayload, resourcePath, context + featureId, messageSubject, inputPayloadSupplier.get(), resourcePath, context ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureActionInput")) )) .orElseGet(DefaultWotThingModelValidator::success); @@ -515,7 +516,7 @@ public CompletionStage validateFeatureActionOutput(@Nullable final ThingDe @Nullable final FeatureDefinition featureDefinition, final String featureId, final String messageSubject, - @Nullable final JsonValue inputPayload, + final Supplier inputPayloadSupplier, final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { @@ -526,7 +527,7 @@ public CompletionStage validateFeatureActionOutput(@Nullable final ThingDe dittoHeaders, featureThingModel -> selectValidation(validationConfig) .validateFeatureActionOutput(featureThingModel, - featureId, messageSubject, inputPayload, resourcePath, context + featureId, messageSubject, inputPayloadSupplier.get(), resourcePath, context ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureActionOutput")) )) .orElseGet(DefaultWotThingModelValidator::success); @@ -537,7 +538,7 @@ public CompletionStage validateFeatureEventData(@Nullable final ThingDefin @Nullable final FeatureDefinition featureDefinition, final String featureId, final String messageSubject, - @Nullable final JsonValue dataPayload, + final Supplier dataPayloadSupplier, final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { @@ -548,7 +549,7 @@ public CompletionStage validateFeatureEventData(@Nullable final ThingDefin dittoHeaders, featureThingModel -> selectValidation(validationConfig) .validateFeatureEventData(featureThingModel, - featureId, messageSubject, dataPayload, resourcePath, context + featureId, messageSubject, dataPayloadSupplier.get(), resourcePath, context ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureEventData")) )) .orElseGet(DefaultWotThingModelValidator::success); diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java index a7aa8bfb8a..a131225212 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java @@ -14,6 +14,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; +import java.util.function.Supplier; import javax.annotation.Nullable; @@ -197,7 +198,7 @@ CompletionStage validateThingScopedDeletion(@Nullable ThingDefinition thin * * @param thingDefinition the ThingDefinition to retrieve the WoT TM from * @param messageSubject the (Thing) message subject - * @param inputPayload the input payload to validate + * @param inputPayloadSupplier the supplier of the input payload to validate * @param resourcePath the originating path of the command which caused validation * @param dittoHeaders the DittoHeaders to use in order to build a potential exception * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a @@ -205,7 +206,7 @@ CompletionStage validateThingScopedDeletion(@Nullable ThingDefinition thin */ CompletionStage validateThingActionInput(@Nullable ThingDefinition thingDefinition, String messageSubject, - @Nullable JsonValue inputPayload, + Supplier inputPayloadSupplier, JsonPointer resourcePath, DittoHeaders dittoHeaders ); @@ -216,7 +217,7 @@ CompletionStage validateThingActionInput(@Nullable ThingDefinition thingDe * * @param thingDefinition the ThingDefinition to retrieve the WoT TM from * @param messageSubject the (Thing) message subject - * @param outputPayload the output payload to validate + * @param outputPayloadSupplier the supplier of the output payload to validate * @param resourcePath the originating path of the command which caused validation * @param dittoHeaders the DittoHeaders to use in order to build a potential exception * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a @@ -224,7 +225,7 @@ CompletionStage validateThingActionInput(@Nullable ThingDefinition thingDe */ CompletionStage validateThingActionOutput(@Nullable ThingDefinition thingDefinition, String messageSubject, - @Nullable JsonValue outputPayload, + Supplier outputPayloadSupplier, JsonPointer resourcePath, DittoHeaders dittoHeaders ); @@ -235,7 +236,7 @@ CompletionStage validateThingActionOutput(@Nullable ThingDefinition thingD * * @param thingDefinition the ThingDefinition to retrieve the WoT TM from * @param messageSubject the (Thing) message subject - * @param dataPayload the data payload to validate + * @param dataPayloadSupplier the supplier of data payload to validate * @param resourcePath the originating path of the command which caused validation * @param dittoHeaders the DittoHeaders to use in order to build a potential exception * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a @@ -243,7 +244,7 @@ CompletionStage validateThingActionOutput(@Nullable ThingDefinition thingD */ CompletionStage validateThingEventData(@Nullable ThingDefinition thingDefinition, String messageSubject, - @Nullable JsonValue dataPayload, + Supplier dataPayloadSupplier, JsonPointer resourcePath, DittoHeaders dittoHeaders ); @@ -459,7 +460,7 @@ CompletionStage validateFeatureScopedDeletion(@Nullable ThingDefinition th * @param featureDefinition the FeatureDefinition to retrieve the WoT TM from * @param featureId the ID of the feature to validate the message input payload for * @param messageSubject the (Feature) message subject - * @param inputPayload the input payload to validate + * @param inputPayloadSupplier the supplier of input payload to validate * @param resourcePath the originating path of the command which caused validation * @param dittoHeaders the DittoHeaders to use in order to build a potential exception * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a @@ -469,7 +470,7 @@ CompletionStage validateFeatureActionInput(@Nullable ThingDefinition thing @Nullable FeatureDefinition featureDefinition, String featureId, String messageSubject, - @Nullable JsonValue inputPayload, + Supplier inputPayloadSupplier, JsonPointer resourcePath, DittoHeaders dittoHeaders ); @@ -482,7 +483,7 @@ CompletionStage validateFeatureActionInput(@Nullable ThingDefinition thing * @param featureDefinition the FeatureDefinition to retrieve the WoT TM from * @param featureId the ID of the feature to validate the message output payload for * @param messageSubject the (Feature) message subject - * @param outputPayload the output payload to validate + * @param outputPayloadSupplier the supplier of output payload to validate * @param resourcePath the originating path of the command which caused validation * @param dittoHeaders the DittoHeaders to use in order to build a potential exception * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a @@ -492,7 +493,7 @@ CompletionStage validateFeatureActionOutput(@Nullable ThingDefinition thin @Nullable FeatureDefinition featureDefinition, String featureId, String messageSubject, - @Nullable JsonValue outputPayload, + Supplier outputPayloadSupplier, JsonPointer resourcePath, DittoHeaders dittoHeaders ); @@ -505,7 +506,7 @@ CompletionStage validateFeatureActionOutput(@Nullable ThingDefinition thin * @param featureDefinition the FeatureDefinition to retrieve the WoT TM from * @param featureId the ID of the feature to validate the message payload for * @param messageSubject the (Feature) message subject - * @param dataPayload the data payload to validate + * @param dataPayloadSupplier the supplier of data payload to validate * @param resourcePath the originating path of the command which caused validation * @param dittoHeaders the DittoHeaders to use in order to build a potential exception * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a @@ -515,7 +516,7 @@ CompletionStage validateFeatureEventData(@Nullable ThingDefinition thingDe @Nullable FeatureDefinition featureDefinition, String featureId, String messageSubject, - @Nullable JsonValue dataPayload, + Supplier dataPayloadSupplier, JsonPointer resourcePath, DittoHeaders dittoHeaders );