diff --git a/base/model/src/main/java/org/eclipse/ditto/base/model/signals/commands/streaming/SubscribeForPersistedEvents.java b/base/model/src/main/java/org/eclipse/ditto/base/model/signals/commands/streaming/SubscribeForPersistedEvents.java index d41b6f7d57..9ee3dd0cc2 100755 --- a/base/model/src/main/java/org/eclipse/ditto/base/model/signals/commands/streaming/SubscribeForPersistedEvents.java +++ b/base/model/src/main/java/org/eclipse/ditto/base/model/signals/commands/streaming/SubscribeForPersistedEvents.java @@ -384,11 +384,6 @@ protected void appendPayload(final JsonObjectBuilder jsonObjectBuilder, getFilter().ifPresent(theFilter -> jsonObjectBuilder.set(JsonFields.FILTER, theFilter)); } - @Override - public String getTypePrefix() { - return TYPE_PREFIX; - } - @Override public SubscribeForPersistedEvents setDittoHeaders(final DittoHeaders dittoHeaders) { return new SubscribeForPersistedEvents(entityId, resourcePath, fromHistoricalRevision, toHistoricalRevision, diff --git a/base/service/pom.xml b/base/service/pom.xml index 566bbb9f20..4428d44269 100644 --- a/base/service/pom.xml +++ b/base/service/pom.xml @@ -38,6 +38,10 @@ org.eclipse.ditto ditto-internal-utils-metrics + + org.eclipse.ditto + ditto-internal-utils-metrics-service + org.eclipse.ditto ditto-internal-utils-tracing diff --git a/base/service/src/main/java/org/eclipse/ditto/base/service/DittoService.java b/base/service/src/main/java/org/eclipse/ditto/base/service/DittoService.java index 2dd6e3e368..01f8ea9d1f 100644 --- a/base/service/src/main/java/org/eclipse/ditto/base/service/DittoService.java +++ b/base/service/src/main/java/org/eclipse/ditto/base/service/DittoService.java @@ -47,7 +47,7 @@ import org.eclipse.ditto.internal.utils.config.raw.RawConfigSupplier; import org.eclipse.ditto.internal.utils.health.status.StatusSupplierActor; import org.eclipse.ditto.internal.utils.metrics.config.MetricsConfig; -import org.eclipse.ditto.internal.utils.metrics.prometheus.PrometheusReporterRoute; +import org.eclipse.ditto.internal.utils.metrics.service.prometheus.PrometheusReporterRoute; import org.eclipse.ditto.internal.utils.tracing.DittoTracing; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/bom/pom.xml b/bom/pom.xml index a03776ee46..f15423ec5b 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -27,7 +27,7 @@ 2.13 - 2.13.12 + 2.13.14 1.1.2 1.0.2 @@ -36,7 +36,8 @@ 0.9.5 - 2.16.1 + 2.17.1 + 1.5.1 1.4.3 0.6.1 3.6.1 @@ -45,8 +46,8 @@ 0.3.0 1.8.0 - 1.0.2 - 1.0.0 + 1.0.3 + 1.0.1 1.2.0 1.0.0 1.0.0 @@ -62,7 +63,7 @@ 2.26.21 - 0.12.5 + 0.12.6 9.2 1.11.0 7.0.0 @@ -78,7 +79,7 @@ 3.1.11 - 2.7.0 + 2.7.1 3.0.2 @@ -133,6 +134,22 @@ import + + com.networknt + json-schema-validator + ${json-schema-validator.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.apache.commons + commons-lang3 + + + + com.typesafe config @@ -540,11 +557,21 @@ ditto-rql-query ${project.version} + + org.eclipse.ditto + ditto-wot-api + ${project.version} + org.eclipse.ditto ditto-wot-model ${project.version} + + org.eclipse.ditto + ditto-wot-validation + ${project.version} + org.eclipse.ditto ditto-wot-integration @@ -618,6 +645,11 @@ ditto-internal-utils-http ${project.version} + + org.eclipse.ditto + ditto-internal-utils-json + ${project.version} + org.eclipse.ditto ditto-internal-utils-jwt @@ -668,6 +700,11 @@ ditto-internal-utils-metrics ${project.version} + + org.eclipse.ditto + ditto-internal-utils-metrics-service + ${project.version} + org.eclipse.ditto ditto-internal-utils-extension diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/exceptions/ConnectionsAmountIllegalException.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/exceptions/ConnectionsAmountIllegalException.java index 1f2c5e2ce5..2416180577 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/exceptions/ConnectionsAmountIllegalException.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/exceptions/ConnectionsAmountIllegalException.java @@ -41,7 +41,7 @@ public final class ConnectionsAmountIllegalException extends DittoRuntimeExcepti public static final String ERROR_CODE = ERROR_CODE_PREFIX + "connections.amount.illegal"; private static final String MESSAGE_TEMPLATE = - "The amount of requested exceptions exceeds the limit of ''{0}''."; + "The amount of requested connections exceeds the limit of ''{0}''."; private static final String DEFAULT_DESCRIPTION = "Please request less connection ids."; diff --git a/connectivity/service/pom.xml b/connectivity/service/pom.xml index fe4984a3cf..afd3837629 100644 --- a/connectivity/service/pom.xml +++ b/connectivity/service/pom.xml @@ -14,12 +14,12 @@ + 4.0.0 ditto-connectivity org.eclipse.ditto ${revision} - 4.0.0 ditto-connectivity-service Eclipse Ditto :: Connectivity :: Service @@ -55,6 +55,10 @@ org.eclipse.ditto ditto-wot-model + + org.eclipse.ditto + ditto-wot-validation + org.eclipse.ditto @@ -69,6 +73,10 @@ org.eclipse.ditto ditto-internal-models-signalenrichment + + org.eclipse.ditto + ditto-internal-utils-http + org.eclipse.ditto ditto-internal-utils-persistence diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfig.java index 89e75ba647..334ec3fa14 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfig.java @@ -19,10 +19,10 @@ import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.base.service.config.http.DefaultHttpProxyConfig; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; import org.eclipse.ditto.internal.utils.config.ScopedConfig; +import org.eclipse.ditto.internal.utils.http.config.DefaultHttpProxyConfig; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; import com.typesafe.config.Config; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HttpPushConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HttpPushConfig.java index 35cd538632..06eaa7b7cf 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HttpPushConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HttpPushConfig.java @@ -16,8 +16,8 @@ import java.util.List; import java.util.Map; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; import com.typesafe.config.Config; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/BasePublisherActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/BasePublisherActor.java index fb61b75620..8aa5776779 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/BasePublisherActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/BasePublisherActor.java @@ -34,6 +34,10 @@ import javax.annotation.Nullable; +import org.apache.pekko.actor.AbstractActor; +import org.apache.pekko.actor.ActorRef; +import org.apache.pekko.actor.ActorSelection; +import org.apache.pekko.japi.pf.ReceiveBuilder; import org.eclipse.ditto.base.model.acks.AcknowledgementLabel; import org.eclipse.ditto.base.model.acks.AcknowledgementRequest; import org.eclipse.ditto.base.model.acks.DittoAcknowledgementLabel; @@ -73,9 +77,9 @@ import org.eclipse.ditto.connectivity.service.messaging.validation.ConnectionValidator; import org.eclipse.ditto.connectivity.service.placeholders.ConnectivityPlaceholders; import org.eclipse.ditto.connectivity.service.util.ConnectivityMdcEntryKey; +import org.eclipse.ditto.internal.utils.config.InstanceIdentifierSupplier; import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLoggingAdapter; -import org.eclipse.ditto.internal.utils.config.InstanceIdentifierSupplier; import org.eclipse.ditto.internal.utils.tracing.DittoTracing; import org.eclipse.ditto.internal.utils.tracing.span.SpanOperationName; import org.eclipse.ditto.internal.utils.tracing.span.SpanTagKey; @@ -86,11 +90,6 @@ import org.eclipse.ditto.things.model.signals.commands.ThingCommand; import org.eclipse.ditto.thingsearch.model.signals.events.SubscriptionEvent; -import org.apache.pekko.actor.AbstractActor; -import org.apache.pekko.actor.ActorRef; -import org.apache.pekko.actor.ActorSelection; -import org.apache.pekko.japi.pf.ReceiveBuilder; - /** * Base class for publisher actors. Holds the map of configured targets. * @@ -474,10 +473,13 @@ private SendingOrDropped publishToGenericTarget(final ExpressionResolver resolve final ExternalMessage mappedMessage = applyHeaderMapping(resolver, outbound, headerMapping); final var startedSpan = DittoTracing.newPreparedSpan( mappedMessage.getHeaders(), - SpanOperationName.of(connection.getConnectionType() + "_publish") + SpanOperationName.of("publish " + connection.getConnectionType() + " " + + outboundSource.getType() + ) ) .connectionId(connection.getId()) .tag(SpanTagKey.CONNECTION_TYPE.getTagForValue(connection.getConnectionType())) + .tag(SpanTagKey.CONNECTION_TARGET.getTagForValue(publishTarget.toString())) .start(); final var mappedMessageWithTraceContext = mappedMessage.withHeaders(startedSpan.propagateContext(mappedMessage.getHeaders())); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/MappingTimer.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/MappingTimer.java index 139567071e..98f566a8b7 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/MappingTimer.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/MappingTimer.java @@ -16,11 +16,14 @@ import java.util.Map; import java.util.function.Supplier; +import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.headers.DittoHeadersBuilder; 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.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.internal.utils.metrics.DittoMetrics; @@ -43,7 +46,7 @@ final class MappingTimer { private static final Logger LOGGER = LoggerFactory.getLogger(MappingTimer.class); - private static final String TIMER_NAME = "connectivity_message_mapping"; + private static final String TIMER_NAME = "message_mapping"; private static final String INBOUND = "inbound"; private static final String OUTBOUND = "outbound"; private static final String PAYLOAD_SEGMENT_NAME = "payload"; @@ -128,34 +131,42 @@ T overall(final Supplier supplier) { * The current span context is attached to each resulting external messages. * * @param mapper ID of the used mapper. + * @param outboundSignal the outbound signal which was mapped. * @param supplier the supplier which is invoked and measured. * @return the result of the supplier. */ - List outboundPayload(final String mapper, final Supplier> supplier) { - final var startedTimer = startNewTimerSegment(mapper); - startedSpan = spawnChildSpanFromStartedTimer(startedTimer); + List outboundPayload(final String mapper, + final OutboundSignal.Mappable outboundSignal, + final Supplier> supplier + ) { + final var startedTimer = timer.startNewSegment(PAYLOAD_SEGMENT_NAME).tag(MAPPER_TAG_NAME, mapper); + startedSpan = spawnChildSpanFromStartedTimer(startedTimer, PAYLOAD_SEGMENT_NAME + " " + mapper); return timed( startedTimer, () -> { final var externalMessages = supplier.get(); return externalMessages.stream() - .map(this::propagateContextToExternalMessage) - .toList(); + .map(externalMessage -> + propagateContextToExternalMessage(externalMessage, + outboundSignal.getSource().getDittoHeaders().getTraceParent().orElse(null)) + ).toList(); } ); } - private StartedTimer startNewTimerSegment(final String mapperName) { - return timer.startNewSegment(PAYLOAD_SEGMENT_NAME).tag(MAPPER_TAG_NAME, mapperName); - } - - private StartedSpan spawnChildSpanFromStartedTimer(final StartedTimer startedTimer) { - final var preparedSpan = startedSpan.spawnChild(SpanOperationName.of(startedTimer.getName())); + private StartedSpan spawnChildSpanFromStartedTimer(final StartedTimer startedTimer, final String segmentName) { + final var preparedSpan = + startedSpan.spawnChild(SpanOperationName.of(startedTimer.getName() + " " + segmentName)); return preparedSpan.startBy(startedTimer); } - private ExternalMessage propagateContextToExternalMessage(final ExternalMessage externalMessage) { - return externalMessage.withHeaders(startedSpan.propagateContext(externalMessage.getHeaders())); + private ExternalMessage propagateContextToExternalMessage(final ExternalMessage externalMessage, + @Nullable final String formerTraceParent) { + final DittoHeadersBuilder dittoHeadersBuilder = DittoHeaders.newBuilder(externalMessage.getHeaders()); + if (formerTraceParent != null) { + dittoHeadersBuilder.traceparent(formerTraceParent); + } + return externalMessage.withHeaders(startedSpan.propagateContext(dittoHeadersBuilder.build())); } /** @@ -167,8 +178,8 @@ private ExternalMessage propagateContextToExternalMessage(final ExternalMessage * @return the list of mapped adaptables */ List inboundPayload(final String mapper, final Supplier> supplier) { - final var startedTimer = startNewTimerSegment(mapper); - startedSpan = spawnChildSpanFromStartedTimer(startedTimer); + final var startedTimer = timer.startNewSegment(PAYLOAD_SEGMENT_NAME).tag(MAPPER_TAG_NAME, mapper); + startedSpan = spawnChildSpanFromStartedTimer(startedTimer, PAYLOAD_SEGMENT_NAME + " " + mapper); return timed( startedTimer, () -> { @@ -193,7 +204,7 @@ private Adaptable propagateContextToAdaptable(final Adaptable adaptable) { */ Signal inboundProtocol(final Supplier> supplier) { final var startedTimer = timer.startNewSegment(PROTOCOL_SEGMENT_NAME); - startedSpan = spawnChildSpanFromStartedTimer(startedTimer); + startedSpan = spawnChildSpanFromStartedTimer(startedTimer, PROTOCOL_SEGMENT_NAME); return timed(startedTimer, () -> propagateContextToSignalDittoHeaders(supplier.get())); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessor.java index eafc7e8634..534921400d 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/OutboundMappingProcessor.java @@ -21,6 +21,8 @@ import javax.annotation.Nullable; +import org.apache.pekko.actor.ActorSelection; +import org.apache.pekko.actor.ActorSystem; import org.eclipse.ditto.base.model.acks.AcknowledgementLabel; import org.eclipse.ditto.base.model.acks.AcknowledgementRequest; import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; @@ -47,9 +49,6 @@ import org.eclipse.ditto.protocol.ProtocolFactory; import org.eclipse.ditto.protocol.adapter.ProtocolAdapter; -import org.apache.pekko.actor.ActorSelection; -import org.apache.pekko.actor.ActorSystem; - /** * Processes outgoing {@link Signal}s to {@link ExternalMessage}s. * Encapsulates the message processing logic from the message mapping processor actor. @@ -229,7 +228,7 @@ private Stream> runMapper(final OutboundSi .debug("Applying mapper <{}> to message <{}>", mapper.getId(), adaptable); final List messages = - timer.outboundPayload(mapper.getId(), () -> checkForNull(mapper.map(adaptable))); + timer.outboundPayload(mapper.getId(), outboundSignal, () -> checkForNull(mapper.map(adaptable))); logger.withCorrelationId(adaptable) .debug("Mapping <{}> produced <{}> messages.", mapper.getId(), messages.size()); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushClientActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushClientActor.java index 36ca0bd024..c22a436a70 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushClientActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushClientActor.java @@ -39,7 +39,6 @@ import org.apache.pekko.stream.javadsl.Sink; import org.apache.pekko.stream.javadsl.Source; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.signals.commands.modify.TestConnection; import org.eclipse.ditto.connectivity.service.config.HttpPushConfig; @@ -50,6 +49,7 @@ import org.eclipse.ditto.connectivity.service.messaging.internal.ssl.SSLContextCreator; import org.eclipse.ditto.connectivity.service.messaging.monitoring.ConnectionMonitor; import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.InfoProviderFactory; +import org.eclipse.ditto.internal.utils.config.http.HttpProxyBaseConfig; import com.typesafe.config.Config; @@ -215,7 +215,7 @@ private CompletionStage testSSL(final Connection connection, fina } private CompletionStage connectViaProxy(final String hostWithoutLookup, final int port) { - final HttpProxyConfig httpProxyConfig = this.httpPushConfig.getHttpProxyConfig(); + final HttpProxyBaseConfig httpProxyConfig = this.httpPushConfig.getHttpProxyConfig(); try (final Socket proxySocket = new Socket(httpProxyConfig.getHostname(), httpProxyConfig.getPort())) { String proxyConnect = "CONNECT " + hostWithoutLookup + ":" + port + " HTTP/1.1\n"; proxyConnect += "Host: " + hostWithoutLookup + ":" + port; diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 411aa82270..ee3434b22a 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -1272,7 +1272,7 @@ pekko-contrib-mongodb-persistence-connection-remember-snapshots { connection-persistence-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" fork-join-executor { parallelism-min = 4 parallelism-factor = 3.0 @@ -1293,7 +1293,7 @@ rabbit-stats-bounded-mailbox { message-mapping-processor-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" fork-join-executor { # Min number of threads to cap factor-based parallelism number to parallelism-min = 4 @@ -1308,17 +1308,17 @@ message-mapping-processor-dispatcher { jms-connection-handling-dispatcher { # one thread per actor because the actor blocks. type = PinnedDispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" } signal-enrichment-cache-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" } http-push-connection-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" # This executor is meant to be allowed to grow quite big as its limited by the max parallelism of each http connection client. # Limit this parallelism here additionally could lead to confusing results regarding througput of some http connections. @@ -1332,17 +1332,17 @@ http-push-connection-dispatcher { kafka-consumer-dispatcher { type = PinnedDispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" } kafka-producer-dispatcher { type = PinnedDispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" } blocked-namespaces-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" fork-join-executor { # Min number of threads to cap factor-based parallelism number to parallelism-min = 4 diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/ConnectivityServiceGlobalErrorRegistryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/ConnectivityServiceGlobalErrorRegistryTest.java index a0eaf38f37..4f71b56324 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/ConnectivityServiceGlobalErrorRegistryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/ConnectivityServiceGlobalErrorRegistryTest.java @@ -45,6 +45,7 @@ import org.eclipse.ditto.thingsearch.api.QueryTimeExceededException; import org.eclipse.ditto.thingsearch.model.signals.commands.exceptions.InvalidNamespacesException; import org.eclipse.ditto.wot.model.WotThingModelInvalidException; +import org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException; public final class ConnectivityServiceGlobalErrorRegistryTest extends GlobalErrorRegistryTestCases { @@ -81,7 +82,8 @@ public ConnectivityServiceGlobalErrorRegistryTest() { JwtInvalidException.class, IllegalAdaptableException.class, WotThingModelInvalidException.class, - EdgeServiceTimeoutException.class + EdgeServiceTimeoutException.class, + WotThingModelPayloadValidationException.class ); } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfigTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfigTest.java index 6880e52011..7db14ee2cc 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfigTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHttpPushConfigTest.java @@ -21,7 +21,7 @@ import java.util.Map; import org.assertj.core.api.JUnitSoftAssertions; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushFactoryTest.java index 7517bda751..a7c37775fe 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/httppush/HttpPushFactoryTest.java @@ -30,9 +30,27 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.apache.pekko.NotUsed; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.http.impl.engine.client.ProxyConnectionFailedException; +import org.apache.pekko.http.javadsl.Http; +import org.apache.pekko.http.javadsl.ServerBinding; +import org.apache.pekko.http.javadsl.model.HttpMethods; +import org.apache.pekko.http.javadsl.model.HttpRequest; +import org.apache.pekko.http.javadsl.model.HttpResponse; +import org.apache.pekko.http.javadsl.model.StatusCodes; +import org.apache.pekko.http.javadsl.model.headers.Authorization; +import org.apache.pekko.japi.Pair; +import org.apache.pekko.stream.KillSwitches; +import org.apache.pekko.stream.OverflowStrategy; +import org.apache.pekko.stream.javadsl.Flow; +import org.apache.pekko.stream.javadsl.Keep; +import org.apache.pekko.stream.javadsl.Sink; +import org.apache.pekko.stream.javadsl.SinkQueueWithCancel; +import org.apache.pekko.stream.javadsl.Source; +import org.apache.pekko.stream.javadsl.SourceQueueWithComplete; +import org.apache.pekko.testkit.javadsl.TestKit; import org.eclipse.ditto.base.service.config.DittoServiceConfig; -import org.eclipse.ditto.base.service.config.http.DefaultHttpProxyConfig; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; @@ -47,6 +65,8 @@ import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.InfoProviderFactory; import org.eclipse.ditto.connectivity.service.messaging.tunnel.SshTunnelState; import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; +import org.eclipse.ditto.internal.utils.http.config.DefaultHttpProxyConfig; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -54,26 +74,6 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; -import org.apache.pekko.NotUsed; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.http.impl.engine.client.ProxyConnectionFailedException; -import org.apache.pekko.http.javadsl.Http; -import org.apache.pekko.http.javadsl.ServerBinding; -import org.apache.pekko.http.javadsl.model.HttpMethods; -import org.apache.pekko.http.javadsl.model.HttpRequest; -import org.apache.pekko.http.javadsl.model.HttpResponse; -import org.apache.pekko.http.javadsl.model.StatusCodes; -import org.apache.pekko.http.javadsl.model.headers.Authorization; -import org.apache.pekko.japi.Pair; -import org.apache.pekko.stream.KillSwitches; -import org.apache.pekko.stream.OverflowStrategy; -import org.apache.pekko.stream.javadsl.Flow; -import org.apache.pekko.stream.javadsl.Keep; -import org.apache.pekko.stream.javadsl.Sink; -import org.apache.pekko.stream.javadsl.SinkQueueWithCancel; -import org.apache.pekko.stream.javadsl.Source; -import org.apache.pekko.stream.javadsl.SourceQueueWithComplete; -import org.apache.pekko.testkit.javadsl.TestKit; import scala.util.Failure; import scala.util.Try; diff --git a/deployment/helm/ditto/local-values.yaml b/deployment/helm/ditto/local-values.yaml index f0a74b06ba..62b8d560c3 100644 --- a/deployment/helm/ditto/local-values.yaml +++ b/deployment/helm/ditto/local-values.yaml @@ -69,6 +69,26 @@ things: - "org.eclipse.ditto.room" authSubjects: - "connection:some" + wot: + tmValidation: + dynamicConfig: + - validationContext: + dittoHeadersPatterns: + - ditto-originator: "connection:one" + thingDefinitionPatterns: + - "^foo.*bar$" + featureDefinitionPatterns: [ ] + configOverrides: + enabled: true + thing: + enforce: + thing-description-modification: false + attributes: true + forbid: + thing-description-deletion: false + feature: + enforce: + feature-description-modification: false ## ---------------------------------------------------------------------------- ## things-search configuration diff --git a/deployment/helm/ditto/templates/things-deployment.yaml b/deployment/helm/ditto/templates/things-deployment.yaml index 3b175b156d..8e531d2876 100644 --- a/deployment/helm/ditto/templates/things-deployment.yaml +++ b/deployment/helm/ditto/templates/things-deployment.yaml @@ -162,6 +162,36 @@ spec: {{- end }} {{- end }} '-Dditto.things.wot.to-thing-description.json-template={{ .Values.things.config.wot.tdJsonTemplate | replace "\n" "" | replace "\\\"" "\"" }}' + {{- range $dynConfIdx, $dynamicWotTmValidationConfig := .Values.things.config.wot.tmValidation.dynamicConfig }} + {{- if or (gt (len $dynamicWotTmValidationConfig.validationContext.dittoHeadersPatterns) 0) (gt (len $dynamicWotTmValidationConfig.validationContext.thingDefinitionPatterns) 0) (gt (len $dynamicWotTmValidationConfig.validationContext.featureDefinitionPatterns) 0) }} + {{- range $dhpIdx, $dittoHeadersPatterns := $dynamicWotTmValidationConfig.validationContext.dittoHeadersPatterns }} + {{- range $dhpKey, $dhpVal := $dittoHeadersPatterns }} + "{{ printf "%s%d%s%d%s%s=%v" "-Dditto.things.wot.tm-model-validation.dynamic-configuration." $dynConfIdx ".validation-context.ditto-headers-patterns." $dhpIdx "." $dhpKey $dhpVal }}" + {{- end }} + {{- end }} + {{- range $tdpIdx, $thingDefinitionPattern := $dynamicWotTmValidationConfig.validationContext.thingDefinitionPatterns }} + "{{ printf "%s%d%s%d=%v" "-Dditto.things.wot.tm-model-validation.dynamic-configuration." $dynConfIdx ".validation-context.thing-definition-patterns." $tdpIdx $thingDefinitionPattern }}" + {{- end }} + {{- range $fdpIdx, $featureDefinitionPattern := $dynamicWotTmValidationConfig.validationContext.featureDefinitionPatterns }} + "{{ printf "%s%d%s%d=%v" "-Dditto.things.wot.tm-model-validation.dynamic-configuration." $dynConfIdx ".validation-context.feature-definition-patterns." $fdpIdx $featureDefinitionPattern }}" + {{- end }} + {{- range $configOverridesKey, $configOverridesValue := $dynamicWotTmValidationConfig.configOverrides }} + {{- if or (eq (kindOf $configOverridesValue) "map") (eq (kindOf $configOverridesValue) "slice") }} + {{- range $nested1ConfigOverridesKey, $nested1ConfigOverridesValue := $configOverridesValue }} + {{- if or (eq (kindOf $nested1ConfigOverridesValue) "map") (eq (kindOf $nested1ConfigOverridesValue) "slice") }} + {{- range $nested2ConfigOverridesKey, $nested2ConfigOverridesValue := $nested1ConfigOverridesValue }} + "{{ printf "%s%d%s%s%s%s%s%s=%v" "-Dditto.things.wot.tm-model-validation.dynamic-configuration." $dynConfIdx ".config-overrides." $configOverridesKey "." $nested1ConfigOverridesKey "." $nested2ConfigOverridesKey $nested2ConfigOverridesValue }}" + {{- end }} + {{- else }} + "{{ printf "%s%d%s%s%s%s=%v" "-Dditto.things.wot.tm-model-validation.dynamic-configuration." $dynConfIdx ".config-overrides." $configOverridesKey "." $nested1ConfigOverridesKey $nested1ConfigOverridesValue }}" + {{- end }} + {{- end }} + {{- else }} + "{{ printf "%s%d%s%s=%v" "-Dditto.things.wot.tm-model-validation.dynamic-configuration." $dynConfIdx ".config-overrides." $configOverridesKey $configOverridesValue }}" + {{- end }} + {{- end }} + {{- end }} + {{- end }} {{ join " " .Values.things.systemProps }} - name: MONGO_DB_SSL_ENABLED value: "{{ printf "%t" .Values.dbconfig.things.ssl }}" @@ -272,6 +302,52 @@ spec: value: "{{ .Values.things.config.policiesEnforcer.cache.expireAfterAccess }}" - name: THINGS_WOT_TO_THING_DESCRIPTION_BASE_PREFIX value: "{{ .Values.things.config.wot.tdBasePrefix }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_ENABLED + value: "{{ .Values.things.config.wot.tmValidation.enabled }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_TD_MODIFICATION + value: "{{ index .Values.things.config.wot.tmValidation.thing.enforce "thing-description-modification" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_ATTRIBUTES + value: "{{ .Values.things.config.wot.tmValidation.thing.enforce.attributes }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_INBOX_MESSAGES_INPUT + value: "{{ index .Values.things.config.wot.tmValidation.thing.enforce "inbox-messages-input" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_INBOX_MESSAGES_OUTPUT + value: "{{ index .Values.things.config.wot.tmValidation.thing.enforce "inbox-messages-output" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_OUTBOX_MESSAGES + value: "{{ index .Values.things.config.wot.tmValidation.thing.enforce "outbox-messages" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_FORBID_TD_DELETION + value: "{{ index .Values.things.config.wot.tmValidation.thing.forbid "thing-description-deletion" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_FORBID_NON_MODELED_ATTRIBUTES + value: "{{ index .Values.things.config.wot.tmValidation.thing.forbid "non-modeled-attributes" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_FORBID_NON_MODELED_INBOX_MESSAGES + value: "{{ index .Values.things.config.wot.tmValidation.thing.forbid "non-modeled-inbox-messages" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_FORBID_NON_MODELED_OUTBOX_MESSAGES + value: "{{ index .Values.things.config.wot.tmValidation.thing.forbid "non-modeled-outbox-messages" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_FD_MODIFICATION + value: "{{ index .Values.things.config.wot.tmValidation.feature.enforce "feature-description-modification" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_PRESENCE_OF_MODELED_FEATURES + value: "{{ index .Values.things.config.wot.tmValidation.feature.enforce "presence-of-modeled-features" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_PROPERTIES + value: "{{ .Values.things.config.wot.tmValidation.feature.enforce.properties }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_DESIRED_PROPERTIES + value: "{{ index .Values.things.config.wot.tmValidation.feature.enforce "desired-properties" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_INBOX_MESSAGES_INPUT + value: "{{ index .Values.things.config.wot.tmValidation.feature.enforce "inbox-messages-input" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_INBOX_MESSAGES_OUTPUT + value: "{{ index .Values.things.config.wot.tmValidation.feature.enforce "inbox-messages-output" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_OUTBOX_MESSAGES + value: "{{ index .Values.things.config.wot.tmValidation.feature.enforce "outbox-messages" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_FD_DELETION + value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "feature-description-deletion" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_NON_MODELED_INBOX_MESSAGES + value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-inbox-messages" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_NON_MODELED_FEATURES + value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-features" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_NON_MODELED_PROPERTIES + value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-properties" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_NON_MODELED_DESIRED_PROPERTIES + value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-desired-properties" }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_NON_MODELED_OUTBOX_MESSAGES + value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-outbox-messages" }}" {{- if .Values.things.extraEnv }} {{- toYaml .Values.things.extraEnv | nindent 12 }} {{- end }} diff --git a/deployment/helm/ditto/values.yaml b/deployment/helm/ditto/values.yaml index 15cc1c511c..59a93f1545 100644 --- a/deployment/helm/ditto/values.yaml +++ b/deployment/helm/ditto/values.yaml @@ -1002,6 +1002,92 @@ things: "security": "basic_sc", "support": "https://www.eclipse.dev/ditto/" } + # tmValidation provides configuration settings for WoT (Web of Things) integration regarding the validation of + # Things and Features based on their WoT ThingModels + tmValidation: + # enabled whether the ThingModel validation of Things/Features should be enabled + enabled: true + # thing provides configuration settings for WoT based validation of Things + thing: + # enforce holds all configuration relating to enforcing the model + enforce: + # thing-description-modification whether to enforce/validate a thing whenever its description is modified + thing-description-modification: true + # attributes whether to enforce/validate attributes of a thing following the defined WoT properties + attributes: true + # inbox-messages-input whether to enforce/validate inbox messages to a thing following the defined WoT action "input" + inbox-messages-input: true + # inbox-messages-output whether to enforce/validate inbox message responses to a thing following the defined WoT action "output" + inbox-messages-output: true + # outbox-messages whether to enforce/validate outbox messages from a thing following the defined WoT event "data" + outbox-messages: true + # forbid holds all configuration relating to forbidding/preventing certain interactions + forbid: + # thing-description-deletion whether to forbid deletion of a thing's description + thing-description-deletion: true + # non-modeled-attributes whether to forbid persisting attributes which are not defined as properties in the WoT model + non-modeled-attributes: true + # non-modeled-inbox-messages whether to forbid dispatching of inbox messages which are not defined as actions in the WoT model + non-modeled-inbox-messages: true + # non-modeled-outbox-messages whether to forbid dispatching of outbox messages which are not defined as events in the WoT model + non-modeled-outbox-messages: true + # feature provides configuration settings for WoT based validation of Features + feature: + # enforce holds all configuration relating to enforcing the model + enforce: + # feature-description-modification whether to enforce/validate a feature whenever its description is modified + feature-description-modification: true + # presence-of-modeled-features whether to enforce that all modeled features + # (submodels referenced in the thing's definition's WoT model) are present + presence-of-modeled-features: true + # properties whether to enforce/validate properties of a feature following the defined WoT properties + properties: true + # desired-properties whether to enforce/validate desired properties of a feature following the defined WoT properties + desired-properties: true + # inbox-messages-input whether to enforce/validate inbox messages to a feature following the defined WoT action "input" + inbox-messages-input: true + # inbox-messages-output whether to enforce/validate inbox message responses to a feature following the defined WoT action "output" + inbox-messages-output: true + # outbox-messages whether to enforce/validate outbox messages from a feature following the defined WoT events + outbox-messages: true + # forbid holds all configuration relating to forbidding/preventing certain interactions + forbid: + # feature-description-deletion whether to forbid deletion of a feature's description + feature-description-deletion: true + # non-modeled-features whether to forbid adding features to a Thing which were not defined in its definition's WoT model + non-modeled-features: true + # non-modeled-properties whether to forbid persisting properties which are not defined as properties in the WoT model + non-modeled-properties: true + # non-modeled-desired-properties whether to forbid persisting desired properties which are not defined as properties in the WoT model + non-modeled-desired-properties: true + # non-modeled-inbox-messages whether to forbid dispatching of inbox messages which are not defined as actions in the WoT model + non-modeled-inbox-messages: true + # non-modeled-outbox-messages whether to forbid dispatching of outbox messages which are not defined as events in the WoT model + non-modeled-outbox-messages: true + # dynamicConfig contains an optional array of objects defining `configOverrides` applied when the defined `validationContext` matches an API call + dynamicConfig: [ + # - validationContext: + # # all 3 "patterns" conditions have to match (AND) + # dittoHeadersPatterns: # if any (OR) of the contained headers block match + # # inside the object, all patterns have to match (AND) + # - ditto-originator: "connection:one" + # thingDefinitionPatterns: # if any (OR) of the contained patterns match + # - "^foo.*bar$" + # featureDefinitionPatterns: [ ] # if any (OR) of the contained patterns match + # # if the validation-context "matches" a processed API call, apply the following overrides: + # configOverrides: + # # exactly the same config keys and structure applies as in the static config + # enabled: true + # thing: + # enforce: + # thing-description-modification: false + # attributes: true + # forbid: + # thing-description-deletion: false + # feature: + # enforce: + # feature-description-modification: false + ] ## ---------------------------------------------------------------------------- ## things-search configuration diff --git a/documentation/src/main/resources/pages/ditto/basic-wot-integration.md b/documentation/src/main/resources/pages/ditto/basic-wot-integration.md index f34f5bd7de..dc0ff5bd6b 100644 --- a/documentation/src/main/resources/pages/ditto/basic-wot-integration.md +++ b/documentation/src/main/resources/pages/ditto/basic-wot-integration.md @@ -408,6 +408,242 @@ Function: * if any error happens during the skeleton creation (e.g. a Thing Model can't be downloaded or is invalid), the Thing is created without the skeleton model, just containing the specified `"definition"` +### Thing Model based validation of changes to properties, action and event payloads + +Since Ditto `3.6.0` it is possible to configure Ditto in a way so that all modification to the persisted things are validated +according to a linked to WoT Thing Model (TM). +That way, Ditto can ensure that Ditto managed things **always** comply with a previously defined (WoT Thing) model. + +This takes the WoT integration on a new level, as WoT TMs are not only used to +[create a Thing skeleton when creating a thing](#thing-skeleton-generation-upon-thing-creation), +ensuring the payload and structure of a thing once on creation. +But over the lifetime of a digital twin it is always enforced that the payload follows the model +(e.g. in regard to data types, ranges, patterns, etc.). + +The implementation supports validation or enforcing the following Ditto concepts: +* On Thing level: + * Modification of a Thing's [definition](basic-thing.html#definition) causes validation of the existing thing against the new TM + * Deletion of a Thing's [definition](basic-thing.html#definition) is forbidden (to not be able to bypass validation) + * Thing [attributes](basic-thing.html#attributes): + * based on the linked TM of a Thing's [definition](basic-thing.html#definition), the Thing `attributes` have to + follow the contract defined by the TM `properties` + * by default, non-modeled `attributes` are forbidden to be created/updated + * `attributes` which are not marked as optional (via `"tm:optional"`) are ensured not to be deleted + * Thing [messages](basic-messages.html#sending-messages): + * based on the linked TM of a Thing's [definition](basic-thing.html#definition), the Thing `messages` + * sent to its `inbox` have to follow the contract defined by the TM `actions` + * both, the defined `input` payload and the `output` payload is validated + * by default, non-modeled `inbox` `messages` are forbidden to be sent + * sent from its `outbox` have to follow the contract defined by the TM `events` + * the defined `data` payload is validated + * by default, non-modeled `outbox` `messages` are forbidden to be sent + * When modifying (or merging) the complete Thing or all of the features at once: + * it is ensured that no defined features get effectively "removed" + * it is ensured that only defined and known features are accepted +* On Feature level: + * Modification of a Feature's [definition](basic-feature.html#feature-definition) causes validation of the existing feature against the new TM + * Deletion of a Feature's [definition](basic-feature.html#feature-definition) is forbidden (to not be able to bypass validation) + * Feature [properties](basic-feature.html#feature-properties): + * based on the linked TM of a Feature's [definition](basic-feature.html#feature-definition), the Feature `properties` + have to follow the contract defined by the TM `properties` + * Feature [messages](basic-messages.html#sending-messages): + * based on the linked TM of a Feature's [definition](basic-feature.html#feature-definition), the Feature `messages` + * sent to its `inbox` have to follow the contract defined by the TM `actions` + * both, the defined `input` payload and the `output` payload is validated + * by default, non-modeled `inbox` `messages` are forbidden to be sent + * sent from its `outbox` have to follow the contract defined by the TM `events` + * the defined `data` payload is validated + * by default, non-modeled `outbox` `messages` are forbidden to be sent + +### Thing Model based validation error + +When Ditto detects an API call which was not valid according to the model, an HTTP status code `400` (Bad Request) will +be returned to the caller, containing a description of the encountered validation violation. + +The basic structure of the WoT validation errors is defined by [the error model](basic-errors.html). +The `error` field will always be `"wot:payload.validation.error"` - and the `message` will also always be the same. +The `description` however will contain a specific text and the `"validationDetails"` field contains a map of +erroneous Json pointer paths. + +The list of collected errors might however not be complete (e.g. for the whole thing), as Ditto will for a +complete [Thing](basic-thing.html) update first its `attributes` and then its `featuers` (one after another) +and will fail fast (instead of validating the complete thing) if validation errors are detected. + +An example payload when e.g. sending the wrong datatype for a Thing "attribute" could look like: +```json +{ + "status": 400, + "error": "wot:payload.validation.error", + "message": "The provided payload did not conform to the specified WoT (Web of Things) model.", + "description": "The Thing's attribute contained validation errors, check the validation details.", + "validationDetails": { + "/attributes/serial": [ + ": {type=boolean found, string expected}" + ] + } +} +``` + +An example where e.g. a `"required"` field of a `"type": "object"` WoT property was missing would be, for example: +```json +{ + "status": 400, + "error": "wot:payload.validation.error", + "message": "The provided payload did not conform to the specified WoT (Web of Things) model.", + "description": "The Feature 's property contained validation errors, check the validation details.", + "validationDetails": { + "/features/connectivity/properties/status": [ + ": {required=[required property 'updatedAt' not found, required property 'message' not found]}" + ] + } +} +``` + +If all feature properties should be updated at once, but no payload was provided, this would list the non-optional +WoT properties in the error response: +```json +{ + "status": 400, + "error": "wot:payload.validation.error", + "message": "The provided payload did not conform to the specified WoT (Web of Things) model.", + "description": "Required JSON fields were missing from the Feature 's properties", + "validationDetails": { + "/features/sensor/properties/value": [ + "Feature 's property is non optional and must be provided" + ], + "/features/sensor/properties/updatedAt": [ + "Feature 's property is non optional and must be provided" + ] + } +} +``` + +#### Model evolution with the help of Thing Model based validation + +This new validation will also make it possible to evolute things conforming to a (WoT) model to e.g. a new minor version +with added functionality and also provide means for "migrating" things to breaking (major) model versions. + +When updating the Thing's [definition](basic-thing.html#definition) to another version, the Ditto WoT validation will +check if the payload of the Thing is valid according to the new definition. +If it is not valid (e.g. because a new feature was added to the Thing's model which is not yet in the Thing's payload), +validation will fail with an error: + +Example assuming to update a Thing's `definition` from version `1.0.0` to version `1.1.0` +(assuming where a new submodel `coffeeMaker` was added): +``` +PUT /api/2/things/org.eclipse.ditto:my-thing-1/definition + +payload: +"https://some.domain/some-model-1.1.0.tm.jsonld" +``` + +The response would e.g. be: +```json +{ + "status": 400, + "error": "wot:payload.validation.error", + "message": "The provided payload did not conform to the specified WoT (Web of Things) model.", + "description": "Attempting to update the Thing with missing in the model defined features: [coffeeMaker]" +} +``` + +This could be solved by doing a `PATCH` update instead, updating both the Thing's `definition` together with the +missing payload (assuming the `coffee-1.0.0` model contains just a required property `counter`): +``` +PATCH /api/2/things/org.eclipse.ditto:my-thing-1 + +headers: +Content-Type: application/merge-patch+json + +payload: +{ + "definition": "https://some.domain/some-model-1.1.0.tm.jsonld", + "features": { + "coffeeMaker": { + "definition": [ + "https://some.domain/submodels/coffee-1.0.0.tm.jsonld" + ], + "properties": { + "counter": 0 + } + } + } +} +``` + +#### Configuration of Thing Model based validation + +Starting with Ditto `3.6.0`, the WoT based validation against Thing Models is enabled by default. +It however can be completely disabled by configuring the environment variable `THINGS_WOT_TM_MODEL_VALIDATION_ENABLED` to `false`. + +Every single validation aspect is configurable separately in Ditto (by default all aspects are enabled). +Please have a look at e.g. the Helm chart configuration, level `things.config.wot.tmValidation`, in order to find all +available configuration options. +Or check the [things.conf](https://github.com/eclipse-ditto/ditto/blob/master/things/service/src/main/resources/things.conf) +(key `ditto.things.wot.tm-model-validation`) to also find out the options and with which environment variables they +can be overridden. + +The configuration can also be applied dynamically based on: +* the presence of certain Ditto headers (e.g. a `ditto-originator` - the connection or user which "caused" an API call) +* the name/URL of the Thing's WoT Thing Model (TM) +* the name/URL of the Feature's WoT Thing Model (TM) + +And example for the dynamic configuration, which would override the static configuration for +* a specific user doing the API call +* AND doing the API call to a Thing with a specific model + +would be the following HOCON configuration: +```hocon +things { + wot { + tm-model-validation { + enabled = true + + dynamic-configuration = [ + { + validation-context { + // all 3 "patterns" conditions have to match (AND) + ditto-headers-patterns = [ // if any (OR) of the contained headers block match + { + // inside the object, all patterns have to match (AND) + ditto-originator = "^pre:ditto$" + } + ] + thing-definition-patterns = [ // if any (OR) of the contained patterns match + "^https://eclipse-ditto.github.io/ditto-examples/wot/models/floor-lamp-1.0.0.tm.jsonld$" + ] + feature-definition-patterns = [ // if any (OR) of the contained patterns match + ] + } + // if the validation-context "matches" a processed API call, apply the following overrides: + config-overrides { + // enabled = false // we could deactivate the complete WoT Thing Model validation with this config + thing { + // disable some aspects of Thing validation + enforce { + attributes = false + } + forbid { + thing-description-deletion = false + } + } + feature { + // disable some aspects of Feature validation + enforce { + properties = false + } + forbid { + feature-description-deletion = false + } + } + } + } + ] + } + } +} +``` + ## Ditto WoT Extension Ontology diff --git a/gateway/service/pom.xml b/gateway/service/pom.xml index bdb9d68809..3bd8277820 100644 --- a/gateway/service/pom.xml +++ b/gateway/service/pom.xml @@ -101,6 +101,10 @@ org.eclipse.ditto ditto-wot-model + + org.eclipse.ditto + ditto-wot-validation + org.eclipse.ditto ditto-internal-models-signalenrichment diff --git a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/AuthenticationConfig.java b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/AuthenticationConfig.java index 7ce4458a28..41383048f2 100644 --- a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/AuthenticationConfig.java +++ b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/AuthenticationConfig.java @@ -14,8 +14,8 @@ import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; /** * Provides configuration settings for the Gateway authentication. diff --git a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfig.java b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfig.java index ccf2840723..226e850343 100644 --- a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfig.java +++ b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfig.java @@ -17,11 +17,11 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.base.service.config.http.DefaultHttpProxyConfig; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; import org.eclipse.ditto.internal.utils.config.ScopedConfig; import org.eclipse.ditto.internal.utils.config.WithConfigPath; +import org.eclipse.ditto.internal.utils.http.config.DefaultHttpProxyConfig; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; import com.typesafe.config.Config; diff --git a/gateway/service/src/main/resources/gateway.conf b/gateway/service/src/main/resources/gateway.conf index 534454aeef..552cd1684d 100755 --- a/gateway/service/src/main/resources/gateway.conf +++ b/gateway/service/src/main/resources/gateway.conf @@ -475,7 +475,7 @@ pekko.http.client { pekko { actor { default-dispatcher { - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" } deployment { /gatewayRoot/proxy { @@ -604,7 +604,7 @@ include "ditto-edge-service.conf" authentication-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" thread-pool-executor { # minimum number of threads to cap factor-based core number to core-pool-size-min = 4 @@ -620,7 +620,7 @@ authentication-dispatcher { signal-enrichment-cache-dispatcher { type = Dispatcher - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" } include "gateway-extension.conf" diff --git a/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/endpoints/EndpointTestBase.java b/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/endpoints/EndpointTestBase.java index 10a5aee5cf..a00206bcac 100755 --- a/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/endpoints/EndpointTestBase.java +++ b/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/endpoints/EndpointTestBase.java @@ -47,7 +47,6 @@ import org.eclipse.ditto.base.model.signals.commands.AbstractCommandResponse; import org.eclipse.ditto.base.model.signals.commands.Command; import org.eclipse.ditto.base.model.signals.commands.CommandResponse; -import org.eclipse.ditto.base.service.config.http.DefaultHttpProxyConfig; import org.eclipse.ditto.gateway.service.endpoints.routes.RootRouteExceptionHandler; import org.eclipse.ditto.gateway.service.endpoints.routes.RouteBaseProperties; import org.eclipse.ditto.gateway.service.security.authentication.jwt.JwtAuthenticationFactory; @@ -77,6 +76,7 @@ import org.eclipse.ditto.internal.utils.health.cluster.ClusterStatus; import org.eclipse.ditto.internal.utils.http.DefaultHttpClientFacade; import org.eclipse.ditto.internal.utils.http.HttpClientFacade; +import org.eclipse.ditto.internal.utils.http.config.DefaultHttpProxyConfig; import org.eclipse.ditto.internal.utils.protocol.ProtocolAdapterProvider; import org.eclipse.ditto.internal.utils.protocol.config.DefaultProtocolConfig; import org.eclipse.ditto.internal.utils.protocol.config.ProtocolConfig; diff --git a/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/starter/GatewayServiceGlobalErrorRegistryTest.java b/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/starter/GatewayServiceGlobalErrorRegistryTest.java index a1f61db9ea..939a85a15e 100644 --- a/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/starter/GatewayServiceGlobalErrorRegistryTest.java +++ b/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/starter/GatewayServiceGlobalErrorRegistryTest.java @@ -45,6 +45,7 @@ import org.eclipse.ditto.thingsearch.api.QueryTimeExceededException; import org.eclipse.ditto.thingsearch.model.signals.commands.exceptions.InvalidNamespacesException; import org.eclipse.ditto.wot.model.WotThingModelInvalidException; +import org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException; public final class GatewayServiceGlobalErrorRegistryTest extends GlobalErrorRegistryTestCases { @@ -81,7 +82,8 @@ public GatewayServiceGlobalErrorRegistryTest() { UnknownTopicPathException.class, IllegalAdaptableException.class, WotThingModelInvalidException.class, - EdgeServiceTimeoutException.class + EdgeServiceTimeoutException.class, + WotThingModelPayloadValidationException.class ); } diff --git a/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfigTest.java b/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfigTest.java index 0586b53781..de08037dae 100644 --- a/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfigTest.java +++ b/gateway/service/src/test/java/org/eclipse/ditto/gateway/service/util/config/security/DefaultAuthenticationConfigTest.java @@ -17,7 +17,7 @@ import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; import org.assertj.core.api.JUnitSoftAssertions; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; diff --git a/internal/utils/cache-loaders/pom.xml b/internal/utils/cache-loaders/pom.xml index ccca669843..866f78907f 100644 --- a/internal/utils/cache-loaders/pom.xml +++ b/internal/utils/cache-loaders/pom.xml @@ -49,6 +49,11 @@ logback-classic test + + org.apache.pekko + pekko-slf4j_${scala.version} + test + org.apache.pekko pekko-testkit_${scala.version} diff --git a/internal/utils/cache-loaders/src/main/resources/reference.conf b/internal/utils/cache-loaders/src/main/resources/reference.conf index 822e734be2..6d2978e214 100644 --- a/internal/utils/cache-loaders/src/main/resources/reference.conf +++ b/internal/utils/cache-loaders/src/main/resources/reference.conf @@ -3,5 +3,5 @@ ask-with-retry-dispatcher { type = "Dispatcher" - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" } diff --git a/internal/utils/cluster/pom.xml b/internal/utils/cluster/pom.xml index ff46868b7b..ae29b2ab89 100755 --- a/internal/utils/cluster/pom.xml +++ b/internal/utils/cluster/pom.xml @@ -47,6 +47,10 @@ org.eclipse.ditto ditto-internal-utils-health + + org.eclipse.ditto + ditto-internal-utils-json + org.eclipse.ditto ditto-internal-utils-metrics diff --git a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/AbstractJsonifiableWithDittoHeadersSerializer.java b/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/AbstractJsonifiableWithDittoHeadersSerializer.java index 753e361234..71cd5fe63f 100755 --- a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/AbstractJsonifiableWithDittoHeadersSerializer.java +++ b/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/AbstractJsonifiableWithDittoHeadersSerializer.java @@ -26,6 +26,11 @@ import javax.annotation.Nullable; +import org.apache.pekko.actor.ExtendedActorSystem; +import org.apache.pekko.io.BufferPool; +import org.apache.pekko.io.DirectByteBufferPool; +import org.apache.pekko.serialization.ByteBufferSerializer; +import org.apache.pekko.serialization.SerializerWithStringManifest; import org.eclipse.ditto.base.model.exceptions.DittoJsonException; import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; import org.eclipse.ditto.base.model.headers.DittoHeaders; @@ -35,6 +40,7 @@ import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.base.model.json.Jsonifiable; import org.eclipse.ditto.base.model.signals.JsonParsable; +import org.eclipse.ditto.base.model.signals.WithType; import org.eclipse.ditto.base.model.signals.commands.Command; import org.eclipse.ditto.internal.utils.metrics.DittoMetrics; import org.eclipse.ditto.internal.utils.metrics.instruments.counter.Counter; @@ -57,12 +63,6 @@ import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; -import org.apache.pekko.actor.ExtendedActorSystem; -import org.apache.pekko.io.BufferPool; -import org.apache.pekko.io.DirectByteBufferPool; -import org.apache.pekko.serialization.ByteBufferSerializer; -import org.apache.pekko.serialization.SerializerWithStringManifest; - /** * Abstract {@link SerializerWithStringManifest} which handles serializing and deserializing {@link Jsonifiable}s * {@link WithDittoHeaders}. @@ -147,7 +147,7 @@ public String manifest(final Object o) { public void toBinary(final Object object, final ByteBuffer buf) { if (object instanceof Jsonifiable jsonifiable) { final var dittoHeaders = getDittoHeadersOrEmpty(object); - final var startedSpan = startTracingSpanForSerialization(dittoHeaders, object.getClass()); + final var startedSpan = startTracingSpanForSerialization(dittoHeaders, object); final var jsonObject = JsonObject.newBuilder() .set(JSON_DITTO_HEADERS, getDittoHeadersWithSpanContextAsJson(dittoHeaders, startedSpan)) .set(JSON_PAYLOAD, getAsJsonPayload(jsonifiable, dittoHeaders)) @@ -187,12 +187,20 @@ public void toBinary(final Object object, final ByteBuffer buf) { private static StartedSpan startTracingSpanForSerialization( final DittoHeaders dittoHeaders, - final Class typeToSerialize + final Object objectToSerialize ) { + final String spanSerializeName; + if (objectToSerialize instanceof WithType withType) { + spanSerializeName = withType.getType(); + } else if (objectToSerialize instanceof DittoRuntimeException dre) { + spanSerializeName = dre.getErrorCode(); + } else { + spanSerializeName = objectToSerialize.getClass().getSimpleName(); + } final var startInstant = StartInstant.now(); return DittoTracing.newPreparedSpan( dittoHeaders, - SpanOperationName.of("serialize " + typeToSerialize.getSimpleName()) + SpanOperationName.of("serialize " + spanSerializeName) ) .correlationId(dittoHeaders.getCorrelationId().orElse(null)) .startAt(startInstant); diff --git a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializer.java b/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializer.java index 49486887ff..c92143b325 100644 --- a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializer.java +++ b/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializer.java @@ -21,6 +21,13 @@ import java.text.MessageFormat; import java.util.Map; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.actor.ExtendedActorSystem; +import org.apache.pekko.io.BufferPool; +import org.apache.pekko.io.DirectByteBufferPool; +import org.apache.pekko.serialization.ByteBufferSerializer; +import org.apache.pekko.serialization.SerializerWithStringManifest; +import org.eclipse.ditto.internal.utils.json.CborFactoryLoader; import org.eclipse.ditto.internal.utils.metrics.DittoMetrics; import org.eclipse.ditto.internal.utils.metrics.instruments.counter.Counter; import org.eclipse.ditto.internal.utils.metrics.instruments.tag.Tag; @@ -36,13 +43,6 @@ import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.actor.ExtendedActorSystem; -import org.apache.pekko.io.BufferPool; -import org.apache.pekko.io.DirectByteBufferPool; -import org.apache.pekko.serialization.ByteBufferSerializer; -import org.apache.pekko.serialization.SerializerWithStringManifest; - /** * Serializer of Eclipse Ditto for {@link JsonValue}s via CBOR. */ diff --git a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonifiableSerializer.java b/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonifiableSerializer.java index f78ddaa116..7a6e931c95 100644 --- a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonifiableSerializer.java +++ b/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborJsonifiableSerializer.java @@ -15,12 +15,12 @@ import java.io.IOException; import java.nio.ByteBuffer; +import org.apache.pekko.actor.ExtendedActorSystem; +import org.eclipse.ditto.internal.utils.json.CborFactoryLoader; import org.eclipse.ditto.json.CborFactory; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonValue; -import org.apache.pekko.actor.ExtendedActorSystem; - /** * Serializer of Eclipse Ditto for Jsonifiables via CBOR-based {@code ditto-json}. */ diff --git a/internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializerTest.java b/internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializerTest.java index 30ad3015a3..3c7d92dce6 100644 --- a/internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializerTest.java +++ b/internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborJsonValueSerializerTest.java @@ -21,6 +21,10 @@ import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.actor.ExtendedActorSystem; +import org.apache.pekko.testkit.TestKit; +import org.eclipse.ditto.internal.utils.json.CborFactoryLoader; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonValue; import org.junit.AfterClass; @@ -28,9 +32,6 @@ import org.junit.BeforeClass; import org.junit.Test; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.actor.ExtendedActorSystem; -import org.apache.pekko.testkit.TestKit; import scala.concurrent.duration.FiniteDuration; /** diff --git a/internal/utils/config/pom.xml b/internal/utils/config/pom.xml index aebbdf6b16..02078c2148 100755 --- a/internal/utils/config/pom.xml +++ b/internal/utils/config/pom.xml @@ -46,18 +46,6 @@ org.slf4j slf4j-api - - org.apache.pekko - pekko-actor_${scala.version} - - - org.apache.pekko - pekko-lease-kubernetes_${scala.version} - - - org.apache.pekko - pekko-coordination_${scala.version} - diff --git a/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/http/DefaultHttpProxyBaseConfig.java b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/http/DefaultHttpProxyBaseConfig.java new file mode 100644 index 0000000000..5ee63c22a2 --- /dev/null +++ b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/http/DefaultHttpProxyBaseConfig.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 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.internal.utils.config.http; + +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; + +import com.typesafe.config.Config; + +/** + * This class is the default implementation of the HTTP proxy config. + */ +@Immutable +public final class DefaultHttpProxyBaseConfig implements HttpProxyBaseConfig { + + private static final String HTTP_PROXY_PATH = "http.proxy"; + private static final String PROXY_PATH = "proxy"; + + private final boolean enabled; + private final String hostName; + private final int port; + private final String userName; + private final String password; + + private DefaultHttpProxyBaseConfig(final ConfigWithFallback configWithFallback) { + enabled = configWithFallback.getBoolean(HttpProxyConfigValue.ENABLED.getConfigPath()); + hostName = configWithFallback.getString(HttpProxyConfigValue.HOST_NAME.getConfigPath()); + port = configWithFallback.getInt(HttpProxyConfigValue.PORT.getConfigPath()); + userName = configWithFallback.getString(HttpProxyConfigValue.USER_NAME.getConfigPath()); + password = configWithFallback.getString(HttpProxyConfigValue.PASSWORD.getConfigPath()); + } + + /** + * Returns an instance of {@code DefaultHttpProxyConfig} based on the settings of the specified Config. + * + * @param config is supposed to provide the settings of the HTTP proxy config at {@value #HTTP_PROXY_PATH}. + * @return the instance. + * @throws org.eclipse.ditto.internal.utils.config.DittoConfigError if {@code config} is invalid. + */ + public static DefaultHttpProxyBaseConfig ofHttpProxy(final Config config) { + return ofConfigPath(config, HTTP_PROXY_PATH); + } + + public static DefaultHttpProxyBaseConfig ofProxy(final Config config) { + return ofConfigPath(config, PROXY_PATH); + } + + private static DefaultHttpProxyBaseConfig ofConfigPath(final Config config, final String relativePath) { + return new DefaultHttpProxyBaseConfig( + ConfigWithFallback.newInstance(config, relativePath, HttpProxyConfigValue.values())); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public String getHostname() { + return hostName; + } + + @Override + public int getPort() { + return port; + } + + @Override + public String getUsername() { + return userName; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DefaultHttpProxyBaseConfig that = (DefaultHttpProxyBaseConfig) o; + return enabled == that.enabled && + port == that.port && + Objects.equals(hostName, that.hostName) && + Objects.equals(userName, that.userName) && + Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, hostName, port, userName, password); + } + + @SuppressWarnings("squid:S2068") + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "enabled=" + enabled + + ", hostName=" + hostName + + ", port=" + port + + ", userName=" + userName + + ", password=*****" + + "]"; + } + +} diff --git a/base/service/src/main/java/org/eclipse/ditto/base/service/config/http/HttpProxyConfig.java b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/http/HttpProxyBaseConfig.java similarity index 83% rename from base/service/src/main/java/org/eclipse/ditto/base/service/config/http/HttpProxyConfig.java rename to internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/http/HttpProxyBaseConfig.java index 8a67baed53..93af767e59 100644 --- a/base/service/src/main/java/org/eclipse/ditto/base/service/config/http/HttpProxyConfig.java +++ b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/http/HttpProxyBaseConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,19 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.base.service.config.http; +package org.eclipse.ditto.internal.utils.config.http; import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; -import org.apache.pekko.http.javadsl.ClientTransport; - /** * Provides configuration settings for the HTTP proxy. */ @Immutable -public interface HttpProxyConfig { +public interface HttpProxyBaseConfig { /** * Indicates whether the HTTP proxy should be enabled. @@ -59,14 +57,6 @@ public interface HttpProxyConfig { */ String getPassword(); - /** - * Converts the proxy settings to an Pekko HTTP client transport object. - * Does not check whether the proxy is enabled. - * - * @return an Pekko HTTP client transport object matching this config. - */ - ClientTransport toClientTransport(); - /** * An enumeration of the known config path expressions and their associated default values for * {@code HttpProxyConfig}. diff --git a/internal/utils/config/src/main/resources/ditto-pekko-config.conf b/internal/utils/config/src/main/resources/ditto-pekko-config.conf index f32d27946f..0174e5d62d 100644 --- a/internal/utils/config/src/main/resources/ditto-pekko-config.conf +++ b/internal/utils/config/src/main/resources/ditto-pekko-config.conf @@ -113,7 +113,7 @@ pekko { } default-dispatcher { - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" fork-join-executor { parallelism-min = 4 parallelism-factor = 3.0 @@ -277,7 +277,7 @@ sharding-dispatcher { # Dispatcher is the name of the event-based dispatcher type = Dispatcher # What kind of ExecutionService to use - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" # Configuration for the fork join pool fork-join-executor { # Min number of threads to cap factor-based parallelism number to @@ -316,7 +316,7 @@ pekko.contrib.persistence.mongodb.mongo { realtime-enable-persistence = false metrics-builder { - class = "org.eclipse.ditto.internal.utils.metrics.mongo.MongoMetricsBuilder" + class = "org.eclipse.ditto.internal.utils.metrics.service.mongo.MongoMetricsBuilder" class = ${?MONGO_METRICS_BUILDER_CLASS} } } diff --git a/internal/utils/config/src/main/resources/ditto-things-aggregator.conf b/internal/utils/config/src/main/resources/ditto-things-aggregator.conf index 8465237434..be5714d3c7 100644 --- a/internal/utils/config/src/main/resources/ditto-things-aggregator.conf +++ b/internal/utils/config/src/main/resources/ditto-things-aggregator.conf @@ -11,7 +11,7 @@ aggregator-internal-dispatcher { # Dispatcher is the name of the event-based dispatcher type = Dispatcher # What kind of ExecutionService to use - executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator" + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" # Configuration for the fork join pool fork-join-executor { # Min number of threads to cap factor-based parallelism number to diff --git a/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/http/DefaultHttpProxyBaseConfigTest.java b/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/http/DefaultHttpProxyBaseConfigTest.java new file mode 100644 index 0000000000..c34a4c0590 --- /dev/null +++ b/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/http/DefaultHttpProxyBaseConfigTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 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.internal.utils.config.http; + +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import org.assertj.core.api.JUnitSoftAssertions; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +import junit.framework.TestCase; +import nl.jqno.equalsverifier.EqualsVerifier; + +public class DefaultHttpProxyBaseConfigTest extends TestCase { + + private static Config httpProxyConfig; + + @Rule + public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); + + @BeforeClass + public static void initTestFixture() { + httpProxyConfig = ConfigFactory.load("http-proxy-test"); + } + + @Test + public void assertImmutability() { + assertInstancesOf(DefaultHttpProxyBaseConfig.class, areImmutable()); + } + + @Test + public void testHashCodeAndEquals() { + EqualsVerifier.forClass(DefaultHttpProxyBaseConfig.class) + .usingGetClass() + .verify(); + } + + @Test + public void underTestReturnsDefaultValuesIfBaseConfigWasEmpty() { + final DefaultHttpProxyBaseConfig underTest = DefaultHttpProxyBaseConfig.ofHttpProxy(ConfigFactory.empty()); + + softly.assertThat(underTest.isEnabled()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.ENABLED.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.ENABLED.getDefaultValue()); + softly.assertThat(underTest.getHostname()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.HOST_NAME.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.HOST_NAME.getDefaultValue()); + softly.assertThat(underTest.getPort()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PORT.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.PORT.getDefaultValue()); + softly.assertThat(underTest.getUsername()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.USER_NAME.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.USER_NAME.getDefaultValue()); + softly.assertThat(underTest.getPassword()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PASSWORD.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.PASSWORD.getDefaultValue()); + } + + @Test + public void underTestReturnsValuesOfConfigFile() { + final DefaultHttpProxyBaseConfig underTest = DefaultHttpProxyBaseConfig.ofHttpProxy(httpProxyConfig); + + softly.assertThat(underTest.isEnabled()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.ENABLED.getConfigPath()) + .isTrue(); + softly.assertThat(underTest.getHostname()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.HOST_NAME.getConfigPath()) + .isEqualTo("example.com"); + softly.assertThat(underTest.getPort()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PORT.getConfigPath()) + .isEqualTo(4711); + softly.assertThat(underTest.getUsername()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.USER_NAME.getConfigPath()) + .isEqualTo("john.frume"); + softly.assertThat(underTest.getPassword()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PASSWORD.getConfigPath()) + .isEqualTo("verySecretPW!"); + } +} \ No newline at end of file diff --git a/base/service/src/test/resources/http-proxy-test.conf b/internal/utils/config/src/test/resources/http-proxy-test.conf similarity index 100% rename from base/service/src/test/resources/http-proxy-test.conf rename to internal/utils/config/src/test/resources/http-proxy-test.conf diff --git a/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/DefaultHttpClientFacade.java b/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/DefaultHttpClientFacade.java index b0719d9787..7b5bccea7e 100644 --- a/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/DefaultHttpClientFacade.java +++ b/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/DefaultHttpClientFacade.java @@ -16,13 +16,13 @@ import javax.annotation.Nullable; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; - import org.apache.pekko.actor.ActorSystem; import org.apache.pekko.http.javadsl.Http; import org.apache.pekko.http.javadsl.model.HttpRequest; import org.apache.pekko.http.javadsl.model.HttpResponse; import org.apache.pekko.http.javadsl.settings.ConnectionPoolSettings; +import org.eclipse.ditto.internal.utils.config.http.HttpProxyBaseConfig; +import org.eclipse.ditto.internal.utils.http.config.HttpProxyConfig; /** * Default implementation of {@link HttpClientFacade}. @@ -49,7 +49,7 @@ private DefaultHttpClientFacade(final ActorSystem actorSystem, * @return the instance. */ public static DefaultHttpClientFacade getInstance(final ActorSystem actorSystem, - final HttpProxyConfig httpProxyConfig) { + final HttpProxyBaseConfig httpProxyConfig) { // the HttpClientProvider is only configured at the very first invocation of getInstance(Config) as we can // assume that the config does not change during runtime @@ -62,10 +62,10 @@ public static DefaultHttpClientFacade getInstance(final ActorSystem actorSystem, } private static DefaultHttpClientFacade createInstance(final ActorSystem actorSystem, - final HttpProxyConfig proxyConfig) { + final HttpProxyBaseConfig proxyConfig) { ConnectionPoolSettings connectionPoolSettings = ConnectionPoolSettings.create(actorSystem); - if (proxyConfig.isEnabled()) { - connectionPoolSettings = connectionPoolSettings.withTransport(proxyConfig.toClientTransport()); + if (proxyConfig.isEnabled() && proxyConfig instanceof HttpProxyConfig pekkoHttpProxyConfig) { + connectionPoolSettings = connectionPoolSettings.withTransport(pekkoHttpProxyConfig.toClientTransport()); } return new DefaultHttpClientFacade(actorSystem, connectionPoolSettings); } diff --git a/base/service/src/main/java/org/eclipse/ditto/base/service/config/http/DefaultHttpProxyConfig.java b/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/config/DefaultHttpProxyConfig.java similarity index 97% rename from base/service/src/main/java/org/eclipse/ditto/base/service/config/http/DefaultHttpProxyConfig.java rename to internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/config/DefaultHttpProxyConfig.java index 1045897c1d..11bdd23281 100644 --- a/base/service/src/main/java/org/eclipse/ditto/base/service/config/http/DefaultHttpProxyConfig.java +++ b/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/config/DefaultHttpProxyConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.base.service.config.http; +package org.eclipse.ditto.internal.utils.http.config; import java.net.InetSocketAddress; import java.util.Objects; @@ -18,14 +18,13 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.apache.pekko.http.javadsl.ClientTransport; +import org.apache.pekko.http.javadsl.model.headers.HttpCredentials; import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; import org.eclipse.ditto.internal.utils.config.DittoConfigError; import com.typesafe.config.Config; -import org.apache.pekko.http.javadsl.ClientTransport; -import org.apache.pekko.http.javadsl.model.headers.HttpCredentials; - /** * This class is the default implementation of the HTTP proxy config. */ diff --git a/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/config/HttpProxyConfig.java b/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/config/HttpProxyConfig.java new file mode 100644 index 0000000000..30eb3d7ff7 --- /dev/null +++ b/internal/utils/http/src/main/java/org/eclipse/ditto/internal/utils/http/config/HttpProxyConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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.internal.utils.http.config; + +import javax.annotation.concurrent.Immutable; + +import org.apache.pekko.http.javadsl.ClientTransport; +import org.eclipse.ditto.internal.utils.config.http.HttpProxyBaseConfig; + +/** + * Provides configuration settings for the HTTP proxy with additional Pekko HTTP specifics. + */ +@Immutable +public interface HttpProxyConfig extends HttpProxyBaseConfig { + + /** + * Converts the proxy settings to a Pekko HTTP client transport object. + * Does not check whether the proxy is enabled. + * + * @return a Pekko HTTP client transport object matching this config. + */ + ClientTransport toClientTransport(); + +} diff --git a/base/service/src/test/java/org/eclipse/ditto/base/service/config/http/DefaultHttpProxyConfigTest.java b/internal/utils/http/src/test/java/org/eclipse/ditto/internal/utils/http/config/DefaultHttpProxyConfigTest.java similarity index 62% rename from base/service/src/test/java/org/eclipse/ditto/base/service/config/http/DefaultHttpProxyConfigTest.java rename to internal/utils/http/src/test/java/org/eclipse/ditto/internal/utils/http/config/DefaultHttpProxyConfigTest.java index ee956c4997..de17c920b4 100644 --- a/base/service/src/test/java/org/eclipse/ditto/base/service/config/http/DefaultHttpProxyConfigTest.java +++ b/internal/utils/http/src/test/java/org/eclipse/ditto/internal/utils/http/config/DefaultHttpProxyConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,13 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.base.service.config.http; +package org.eclipse.ditto.internal.utils.http.config; import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; import org.assertj.core.api.JUnitSoftAssertions; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig.HttpProxyConfigValue; +import org.eclipse.ditto.internal.utils.config.http.HttpProxyBaseConfig; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -58,20 +58,20 @@ public void underTestReturnsDefaultValuesIfBaseConfigWasEmpty() { final DefaultHttpProxyConfig underTest = DefaultHttpProxyConfig.ofHttpProxy(ConfigFactory.empty()); softly.assertThat(underTest.isEnabled()) - .as(HttpProxyConfigValue.ENABLED.getConfigPath()) - .isEqualTo(HttpProxyConfigValue.ENABLED.getDefaultValue()); + .as(HttpProxyBaseConfig.HttpProxyConfigValue.ENABLED.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.ENABLED.getDefaultValue()); softly.assertThat(underTest.getHostname()) - .as(HttpProxyConfigValue.HOST_NAME.getConfigPath()) - .isEqualTo(HttpProxyConfigValue.HOST_NAME.getDefaultValue()); + .as(HttpProxyBaseConfig.HttpProxyConfigValue.HOST_NAME.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.HOST_NAME.getDefaultValue()); softly.assertThat(underTest.getPort()) - .as(HttpProxyConfigValue.PORT.getConfigPath()) - .isEqualTo(HttpProxyConfigValue.PORT.getDefaultValue()); + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PORT.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.PORT.getDefaultValue()); softly.assertThat(underTest.getUsername()) - .as(HttpProxyConfigValue.USER_NAME.getConfigPath()) - .isEqualTo(HttpProxyConfigValue.USER_NAME.getDefaultValue()); + .as(HttpProxyBaseConfig.HttpProxyConfigValue.USER_NAME.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.USER_NAME.getDefaultValue()); softly.assertThat(underTest.getPassword()) - .as(HttpProxyConfigValue.PASSWORD.getConfigPath()) - .isEqualTo(HttpProxyConfigValue.PASSWORD.getDefaultValue()); + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PASSWORD.getConfigPath()) + .isEqualTo(HttpProxyBaseConfig.HttpProxyConfigValue.PASSWORD.getDefaultValue()); } @Test @@ -79,19 +79,19 @@ public void underTestReturnsValuesOfConfigFile() { final DefaultHttpProxyConfig underTest = DefaultHttpProxyConfig.ofHttpProxy(httpProxyConfig); softly.assertThat(underTest.isEnabled()) - .as(HttpProxyConfigValue.ENABLED.getConfigPath()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.ENABLED.getConfigPath()) .isTrue(); softly.assertThat(underTest.getHostname()) - .as(HttpProxyConfigValue.HOST_NAME.getConfigPath()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.HOST_NAME.getConfigPath()) .isEqualTo("example.com"); softly.assertThat(underTest.getPort()) - .as(HttpProxyConfigValue.PORT.getConfigPath()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PORT.getConfigPath()) .isEqualTo(4711); softly.assertThat(underTest.getUsername()) - .as(HttpProxyConfigValue.USER_NAME.getConfigPath()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.USER_NAME.getConfigPath()) .isEqualTo("john.frume"); softly.assertThat(underTest.getPassword()) - .as(HttpProxyConfigValue.PASSWORD.getConfigPath()) + .as(HttpProxyBaseConfig.HttpProxyConfigValue.PASSWORD.getConfigPath()) .isEqualTo("verySecretPW!"); } diff --git a/internal/utils/http/src/test/resources/http-proxy-test.conf b/internal/utils/http/src/test/resources/http-proxy-test.conf new file mode 100644 index 0000000000..2d51e8eeb7 --- /dev/null +++ b/internal/utils/http/src/test/resources/http-proxy-test.conf @@ -0,0 +1,9 @@ +http { + proxy { + enabled = true + hostname = "example.com" + port = 4711 + username = "john.frume" + password = "verySecretPW!" + } +} \ No newline at end of file diff --git a/internal/utils/json/pom.xml b/internal/utils/json/pom.xml new file mode 100755 index 0000000000..69c9b9a106 --- /dev/null +++ b/internal/utils/json/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + + org.eclipse.ditto + ditto-internal-utils + ${revision} + + + ditto-internal-utils-json + Eclipse Ditto :: Internal :: Utils :: JSON + + + + org.eclipse.ditto + ditto-json + + + org.eclipse.ditto + ditto-json-cbor + + + + diff --git a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborFactoryLoader.java b/internal/utils/json/src/main/java/org/eclipse/ditto/internal/utils/json/CborFactoryLoader.java similarity index 90% rename from internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborFactoryLoader.java rename to internal/utils/json/src/main/java/org/eclipse/ditto/internal/utils/json/CborFactoryLoader.java index e11e3ba6eb..44dee1c64e 100644 --- a/internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/CborFactoryLoader.java +++ b/internal/utils/json/src/main/java/org/eclipse/ditto/internal/utils/json/CborFactoryLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.internal.utils.cluster; +package org.eclipse.ditto.internal.utils.json; import java.text.MessageFormat; import java.util.ServiceLoader; @@ -25,7 +25,7 @@ * is thrown. */ @ThreadSafe -final class CborFactoryLoader { +public final class CborFactoryLoader { @Nullable private static CborFactoryLoader instance = null; @@ -38,7 +38,7 @@ private CborFactoryLoader() { super(); } - static CborFactoryLoader getInstance() { + public static CborFactoryLoader getInstance() { var result = instance; if (null == result) { result = new CborFactoryLoader(); @@ -47,7 +47,7 @@ static CborFactoryLoader getInstance() { return result; } - CborFactory getCborFactoryOrThrow() { + public CborFactory getCborFactoryOrThrow() { var result = cborFactory; // Double-Check-Idiom diff --git a/internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborFactoryLoaderTest.java b/internal/utils/json/src/test/java/org/eclipse/ditto/internal/utils/json/CborFactoryLoaderTest.java similarity index 91% rename from internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborFactoryLoaderTest.java rename to internal/utils/json/src/test/java/org/eclipse/ditto/internal/utils/json/CborFactoryLoaderTest.java index 782757364e..d9b6c52830 100644 --- a/internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/CborFactoryLoaderTest.java +++ b/internal/utils/json/src/test/java/org/eclipse/ditto/internal/utils/json/CborFactoryLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.internal.utils.cluster; +package org.eclipse.ditto.internal.utils.json; import static org.assertj.core.api.Assertions.assertThat; diff --git a/internal/utils/metrics-service/pom.xml b/internal/utils/metrics-service/pom.xml new file mode 100644 index 0000000000..7caf2fd268 --- /dev/null +++ b/internal/utils/metrics-service/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + + ditto-internal-utils + org.eclipse.ditto + ${revision} + + + ditto-internal-utils-metrics-service + Eclipse Ditto :: Internal :: Utils :: Metrics Service + + + + org.eclipse.ditto + ditto-internal-utils-metrics + + + io.kamon + kamon-prometheus_${scala.version} + + + io.kamon + kamon-executors_${scala.version} + + + org.apache.pekko + pekko-actor_${scala.version} + + + org.apache.pekko + pekko-http_${scala.version} + provided + + + + + com.github.scullxbones + pekko-persistence-mongodb_${scala.version} + provided + + + + nl.grons + metrics4-scala_${scala.version} + + + + diff --git a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/executor/InstrumentedForkJoinExecutorServiceConfigurator.java b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/executor/InstrumentedForkJoinExecutorServiceConfigurator.java similarity index 89% rename from internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/executor/InstrumentedForkJoinExecutorServiceConfigurator.java rename to internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/executor/InstrumentedForkJoinExecutorServiceConfigurator.java index c6df458e43..32d90ef7ab 100644 --- a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/executor/InstrumentedForkJoinExecutorServiceConfigurator.java +++ b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/executor/InstrumentedForkJoinExecutorServiceConfigurator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,16 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.internal.utils.metrics.executor; +package org.eclipse.ditto.internal.utils.metrics.service.executor; import java.util.concurrent.ThreadFactory; -import com.typesafe.config.Config; - import org.apache.pekko.dispatch.DispatcherPrerequisites; import org.apache.pekko.dispatch.ExecutorServiceConfigurator; import org.apache.pekko.dispatch.ExecutorServiceFactory; import org.apache.pekko.dispatch.ForkJoinExecutorConfigurator; + +import com.typesafe.config.Config; + import kamon.instrumentation.executor.ExecutorInstrumentation; /** @@ -37,7 +38,7 @@ * with *
  *   type = Dispatcher
- *   executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator"
+ *   executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
  *   fork-join-executor {
  *     ...
  *   }
diff --git a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/executor/InstrumentedThreadPoolExecutorServiceConfigurator.java b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/executor/InstrumentedThreadPoolExecutorServiceConfigurator.java
similarity index 89%
rename from internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/executor/InstrumentedThreadPoolExecutorServiceConfigurator.java
rename to internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/executor/InstrumentedThreadPoolExecutorServiceConfigurator.java
index 47467e4b08..a7ae657f63 100644
--- a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/executor/InstrumentedThreadPoolExecutorServiceConfigurator.java
+++ b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/executor/InstrumentedThreadPoolExecutorServiceConfigurator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
@@ -10,16 +10,17 @@
  *
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipse.ditto.internal.utils.metrics.executor;
+package org.eclipse.ditto.internal.utils.metrics.service.executor;
 
 import java.util.concurrent.ThreadFactory;
 
-import com.typesafe.config.Config;
-
 import org.apache.pekko.dispatch.DispatcherPrerequisites;
 import org.apache.pekko.dispatch.ExecutorServiceConfigurator;
 import org.apache.pekko.dispatch.ExecutorServiceFactory;
 import org.apache.pekko.dispatch.ThreadPoolExecutorConfigurator;
+
+import com.typesafe.config.Config;
+
 import kamon.instrumentation.executor.ExecutorInstrumentation;
 
 /**
@@ -37,7 +38,7 @@
  * with
  * 
  *   type = Dispatcher
- *   executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
+ *   executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
  *   thread-pool-executor {
  *     ...
  *   }
diff --git a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/mongo/MongoMetricsBuilder.java b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/mongo/MongoMetricsBuilder.java
similarity index 94%
rename from internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/mongo/MongoMetricsBuilder.java
rename to internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/mongo/MongoMetricsBuilder.java
index 1b236b3ae0..c738e028bd 100644
--- a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/mongo/MongoMetricsBuilder.java
+++ b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/mongo/MongoMetricsBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
@@ -10,7 +10,7 @@
  *
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipse.ditto.internal.utils.metrics.mongo;
+package org.eclipse.ditto.internal.utils.metrics.service.mongo;
 
 import java.util.concurrent.atomic.LongAccumulator;
 
diff --git a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/prometheus/PrometheusReporterRoute.java b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/prometheus/PrometheusReporterRoute.java
similarity index 93%
rename from internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/prometheus/PrometheusReporterRoute.java
rename to internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/prometheus/PrometheusReporterRoute.java
index bb8163089c..39a89b4874 100644
--- a/internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/prometheus/PrometheusReporterRoute.java
+++ b/internal/utils/metrics-service/src/main/java/org/eclipse/ditto/internal/utils/metrics/service/prometheus/PrometheusReporterRoute.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
@@ -10,7 +10,7 @@
  *
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipse.ditto.internal.utils.metrics.prometheus;
+package org.eclipse.ditto.internal.utils.metrics.service.prometheus;
 
 import static org.apache.pekko.http.javadsl.server.Directives.complete;
 import static org.apache.pekko.http.javadsl.server.Directives.get;
@@ -21,6 +21,7 @@
 import org.apache.pekko.http.javadsl.model.StatusCodes;
 import org.apache.pekko.http.javadsl.server.Route;
 import org.apache.pekko.util.ByteString;
+
 import kamon.prometheus.PrometheusReporter;
 
 /**
diff --git a/internal/utils/metrics/pom.xml b/internal/utils/metrics/pom.xml
index 8086c514ad..f6a47b225c 100644
--- a/internal/utils/metrics/pom.xml
+++ b/internal/utils/metrics/pom.xml
@@ -14,12 +14,13 @@
 
+    4.0.0
+
     
         ditto-internal-utils
         org.eclipse.ditto
         ${revision}
     
-    4.0.0
 
     ditto-internal-utils-metrics
     Eclipse Ditto :: Internal :: Utils :: Metrics
@@ -37,37 +38,6 @@
             io.kamon
             kamon-core_${scala.version}
         
-        
-            io.kamon
-            kamon-prometheus_${scala.version}
-        
-        
-            io.kamon
-            kamon-executors_${scala.version}
-        
-        
-            org.apache.pekko
-            pekko-actor_${scala.version}
-        
-        
-            org.apache.pekko
-            pekko-http_${scala.version}
-            provided
-        
-
-        
-            
-            com.github.scullxbones
-            pekko-persistence-mongodb_${scala.version}
-            provided
-        
-        
-            
-            nl.grons
-            metrics4-scala_${scala.version}
-        
     
 
 
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/config/DefaultMetricsConfigTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/config/DefaultMetricsConfigTest.java
index d8410f9277..eca88e7ad0 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/config/DefaultMetricsConfigTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/config/DefaultMetricsConfigTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/counter/KamonCounterTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/counter/KamonCounterTest.java
index 8b41794d20..ee78c7a14a 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/counter/KamonCounterTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/counter/KamonCounterTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/gauge/KamonGaugeTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/gauge/KamonGaugeTest.java
index cd20c786b8..f7c11a6a12 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/gauge/KamonGaugeTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/gauge/KamonGaugeTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/histogram/KamonHistogramTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/histogram/KamonHistogramTest.java
index 1d6df9a49a..f443360883 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/histogram/KamonHistogramTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/histogram/KamonHistogramTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/KamonTagSetConverterTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/KamonTagSetConverterTest.java
index 848e5fc8e6..e3092ec492 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/KamonTagSetConverterTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/KamonTagSetConverterTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagSetTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagSetTest.java
index ec7d91724e..f947c6fac0 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagSetTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagSetTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagTest.java
index 09b85a03be..172ca79dcc 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/tag/TagTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/PreparedKamonTimerTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/PreparedKamonTimerTest.java
index 70f19ae919..3908bd6e1a 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/PreparedKamonTimerTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/PreparedKamonTimerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartInstantTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartInstantTest.java
index fbb4f937e4..6593545077 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartInstantTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartInstantTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartedKamonTimerTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartedKamonTimerTest.java
index f926d74848..e060d36861 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartedKamonTimerTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StartedKamonTimerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StoppedKamonTimerTest.java b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StoppedKamonTimerTest.java
index 6e62efc044..0afbee33b8 100644
--- a/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StoppedKamonTimerTest.java
+++ b/internal/utils/metrics/src/test/java/org/eclipse/ditto/internal/utils/metrics/instruments/timer/StoppedKamonTimerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
  *
  * See the NOTICE file(s) distributed with this work for additional
  * information regarding copyright ownership.
diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceActor.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceActor.java
index 8483256c56..0e140f6578 100755
--- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceActor.java
+++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceActor.java
@@ -702,8 +702,9 @@ private > void handleByStrategy(final T command, @Nullable
 
         final var startedSpan = DittoTracing.newPreparedSpan(
                         command.getDittoHeaders(),
-                        SpanOperationName.of("apply_command_strategy")
+                        SpanOperationName.of("apply_command_strategy " + command.getType())
                 )
+                .correlationId(command.getDittoHeaders().getCorrelationId().orElse(null))
                 .start();
 
         final var tracedCommand =
@@ -718,7 +719,7 @@ private > void handleByStrategy(final T command, @Nullable
             final DittoRuntimeException dittoRuntimeException =
                     DittoRuntimeException.asDittoRuntimeException(e, throwable ->
                             DittoInternalErrorException.newBuilder()
-                                    .dittoHeaders(command.getDittoHeaders())
+                                    .dittoHeaders(tracedCommand.getDittoHeaders())
                                     .build());
             startedSpan.tagAsFailed(e);
             result = ResultFactory.newErrorResult(dittoRuntimeException, tracedCommand);
@@ -726,7 +727,7 @@ private > void handleByStrategy(final T command, @Nullable
         } finally {
             startedSpan.finish();
         }
-        reportSudoCommandDone(command);
+        reportSudoCommandDone(tracedCommand);
     }
 
     @Override
diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java
index 5d8936d040..e7c1b917aa 100644
--- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java
+++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java
@@ -842,6 +842,10 @@ private void handleSignalEnforcementResponse(@Nullable final Object response,
                 log.withCorrelationId(dre)
                         .info("Received DittoRuntimeException during enforcement or " +
                                 "forwarding to target actor, telling sender: {}", dre);
+                if (dre instanceof DittoInternalErrorException) {
+                    log.withCorrelationId(dre)
+                            .error(dre, "Received DittoInternalErrorException during enforcement");
+                }
             }
             sender.tell(dre, getSelf());
         } else if (response instanceof Status.Success success) {
@@ -899,18 +903,23 @@ protected CompletionStage enforceSignalAndForwardToTargetActor(final S s
         if (null != enforcerChild) {
             final var startedSpan = DittoTracing.newPreparedSpan(
                             signal.getDittoHeaders(),
-                            SpanOperationName.of(signal.getType())
+                            SpanOperationName.of("process " + signal.getType())
                     )
                     .correlationId(signal.getDittoHeaders().getCorrelationId().orElse(null))
                     .start();
-            final var tracedSignal =
-                    signal.setDittoHeaders(DittoHeaders.of(startedSpan.propagateContext(signal.getDittoHeaders())));
+            final var tracedSignal = signal.setDittoHeaders(
+                    DittoHeaders.of(startedSpan.propagateContext(
+                            signal.getDittoHeaders().toBuilder()
+                                    .removeHeader(DittoHeaderDefinition.W3C_TRACEPARENT.getKey())
+                                    .build()
+                    ))
+            );
             final StartedTimer rootTimer = createTimer(tracedSignal);
             final StartedTimer enforcementTimer = rootTimer.startNewSegment(ENFORCEMENT_TIMER_SEGMENT_ENFORCEMENT);
             return askEnforcerChild(tracedSignal)
                     .thenCompose(this::modifyEnforcerActorEnforcedSignalResponse)
                     .whenComplete((result, error) -> {
-                        startedSpan.mark("enforced");
+                        startedSpan.mark("enforced_policy");
                         stopTimer(enforcementTimer).accept(result, error);
                     })
                     .thenCompose(enforcedCommand -> {
@@ -933,7 +942,7 @@ protected CompletionStage enforceSignalAndForwardToTargetActor(final S s
                                 rootTimer.startNewSegment(ENFORCEMENT_TIMER_SEGMENT_RESPONSE_FILTER);
                         return filterTargetActorResponseViaEnforcer(targetActorResponse)
                                 .whenComplete((result, error) -> {
-                                    startedSpan.mark("response_filtered");
+                                    startedSpan.mark("filtered_response");
                                     responseFilterTimer.tag(ENFORCEMENT_TIMER_TAG_OUTCOME,
                                             error != null ? ENFORCEMENT_TIMER_TAG_OUTCOME_FAIL :
                                                     ENFORCEMENT_TIMER_TAG_OUTCOME_SUCCESS).stop();
diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/cleanup/Credits.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/cleanup/Credits.java
index 6ad4112bc9..f8e6ce4866 100644
--- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/cleanup/Credits.java
+++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/cleanup/Credits.java
@@ -15,14 +15,12 @@
 import java.time.Duration;
 import java.util.concurrent.atomic.LongAccumulator;
 
-import org.eclipse.ditto.internal.utils.pekko.controlflow.Transistor;
-import org.eclipse.ditto.internal.utils.metrics.mongo.MongoMetricsBuilder;
-
 import org.apache.pekko.NotUsed;
 import org.apache.pekko.event.LoggingAdapter;
 import org.apache.pekko.stream.SourceShape;
 import org.apache.pekko.stream.javadsl.GraphDSL;
 import org.apache.pekko.stream.javadsl.Source;
+import org.eclipse.ditto.internal.utils.pekko.controlflow.Transistor;
 
 final class Credits {
 
@@ -36,7 +34,11 @@ final class Credits {
     }
 
     static Credits of(final CleanupConfig config) {
-        return new Credits(config, MongoMetricsBuilder.maxTimerNanos());
+        return new Credits(config, createMaxTimerNanos());
+    }
+
+    private static LongAccumulator createMaxTimerNanos() {
+        return new LongAccumulator(Math::max, 0L);
     }
 
     /**
diff --git a/internal/utils/pom.xml b/internal/utils/pom.xml
index 541e144da9..af8f64bd7d 100755
--- a/internal/utils/pom.xml
+++ b/internal/utils/pom.xml
@@ -35,6 +35,7 @@
         ddata
         health
         http
+        json
         jwt
         namespaces
         persistence
@@ -46,6 +47,7 @@
         test
         tracing
         metrics
+        metrics-service
         persistent-actors
         extension
     
diff --git a/internal/utils/protocol/pom.xml b/internal/utils/protocol/pom.xml
index 56ba2d96ba..536975e372 100755
--- a/internal/utils/protocol/pom.xml
+++ b/internal/utils/protocol/pom.xml
@@ -38,6 +38,11 @@
             com.typesafe
             config
         
+
+        
+            org.apache.pekko
+            pekko-actor_${scala.version}
+        
     
 
 
diff --git a/internal/utils/test/src/test/java/org/eclipse/ditto/internal/utils/test/docker/ContainerFactory.java b/internal/utils/test/src/test/java/org/eclipse/ditto/internal/utils/test/docker/ContainerFactory.java
index 5068bb2a04..6f1ebd67fe 100644
--- a/internal/utils/test/src/test/java/org/eclipse/ditto/internal/utils/test/docker/ContainerFactory.java
+++ b/internal/utils/test/src/test/java/org/eclipse/ditto/internal/utils/test/docker/ContainerFactory.java
@@ -15,6 +15,7 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
@@ -49,7 +50,8 @@ public abstract class ContainerFactory implements Closeable {
     protected ContainerFactory(final String imageIdentifier, final int... ports) {
         this.imageIdentifier = imageIdentifier;
         this.ports = ports;
-        final String dockerHost = OsDetector.isWindows() ? WINDOWS_DOCKER_HOST : UNIX_DOCKER_HOST;
+        final String dockerHost = Objects.requireNonNullElseGet(System.getenv("DOCKER_HOST"),
+                () -> OsDetector.isWindows() ? WINDOWS_DOCKER_HOST : UNIX_DOCKER_HOST);
         LOGGER.info("Connecting to docker daemon on <{}>.", dockerHost);
         final DefaultDockerClientConfig config =
                 DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(dockerHost).build();
diff --git a/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/EmptyStartedSpan.java b/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/EmptyStartedSpan.java
index ad3135e04e..836cb3bb5e 100644
--- a/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/EmptyStartedSpan.java
+++ b/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/EmptyStartedSpan.java
@@ -123,4 +123,10 @@ public int hashCode() {
         return Objects.hash(operationName);
     }
 
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" +
+                "operationName=" + operationName +
+                "]";
+    }
 }
diff --git a/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/SpanTagKey.java b/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/SpanTagKey.java
index 243d3103c8..a1070984fb 100644
--- a/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/SpanTagKey.java
+++ b/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/SpanTagKey.java
@@ -44,6 +44,9 @@ public abstract class SpanTagKey {
     public static final SpanTagKey CONNECTION_TYPE =
             new CharSequenceImplementation(KEY_PREFIX + "connection.type");
 
+    public static final SpanTagKey CONNECTION_TARGET =
+            new CharSequenceImplementation(KEY_PREFIX + "connection.target");
+
     public static final SpanTagKey HTTP_STATUS = new HttpStatusImplementation(KEY_PREFIX + "statusCode");
 
     public static final SpanTagKey REQUEST_METHOD_NAME =
diff --git a/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/StartedKamonSpan.java b/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/StartedKamonSpan.java
index 041c5cbcc6..09d71cac2d 100644
--- a/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/StartedKamonSpan.java
+++ b/internal/utils/tracing/src/main/java/org/eclipse/ditto/internal/utils/tracing/span/StartedKamonSpan.java
@@ -125,4 +125,11 @@ public PreparedSpan spawnChild(final SpanOperationName operationName) {
         return TracingSpans.newPreparedKamonSpan(propagateContext(Map.of()), operationName, httpContextPropagation);
     }
 
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" +
+                "span=" + span +
+                ", httpContextPropagation=" + httpContextPropagation +
+                "]";
+    }
 }
diff --git a/json-cbor/src/main/java/org/eclipse/ditto/json/cbor/JacksonSerializationContext.java b/json-cbor/src/main/java/org/eclipse/ditto/json/cbor/JacksonSerializationContext.java
index 853509dbeb..0ef662ee9b 100644
--- a/json-cbor/src/main/java/org/eclipse/ditto/json/cbor/JacksonSerializationContext.java
+++ b/json-cbor/src/main/java/org/eclipse/ditto/json/cbor/JacksonSerializationContext.java
@@ -24,7 +24,7 @@
 /**
  * Implementation of {@link SerializationContext} backed by Jackson's {@link JsonGenerator}.
  */
-final class JacksonSerializationContext implements SerializationContext {
+public final class JacksonSerializationContext implements SerializationContext {
 
     private final JsonGenerator jacksonGenerator;
     private final ControllableOutputStream outputStream;
diff --git a/messages/model/src/main/java/org/eclipse/ditto/messages/model/Message.java b/messages/model/src/main/java/org/eclipse/ditto/messages/model/Message.java
index 87e41b9d17..41a29eb2a4 100755
--- a/messages/model/src/main/java/org/eclipse/ditto/messages/model/Message.java
+++ b/messages/model/src/main/java/org/eclipse/ditto/messages/model/Message.java
@@ -17,9 +17,10 @@
 import java.time.OffsetDateTime;
 import java.util.Optional;
 
-import org.eclipse.ditto.json.JsonObject;
 import org.eclipse.ditto.base.model.auth.AuthorizationContext;
 import org.eclipse.ditto.base.model.common.HttpStatus;
+import org.eclipse.ditto.base.model.headers.contenttype.ContentType;
+import org.eclipse.ditto.json.JsonObject;
 import org.eclipse.ditto.things.model.ThingId;
 
 /**
@@ -112,6 +113,16 @@ static  MessageBuilder newBuilder(final MessageHeaders messageHeaders) {
      */
     Optional getContentType();
 
+    /**
+     * Returns the content-type of the payload as provided by the message sender as interpreted {@link ContentType}.
+     *
+     * @return the interpreted content type.
+     * @since 3.6.0
+     */
+    default Optional getInterpretedContentType() {
+        return getContentType().map(ContentType::of);
+    }
+
     /**
      * Returns the timeout of the message.
      *
diff --git a/policies/enforcement/src/main/java/org/eclipse/ditto/policies/enforcement/AbstractEnforcerActor.java b/policies/enforcement/src/main/java/org/eclipse/ditto/policies/enforcement/AbstractEnforcerActor.java
index bfd22520ae..323a4bc603 100644
--- a/policies/enforcement/src/main/java/org/eclipse/ditto/policies/enforcement/AbstractEnforcerActor.java
+++ b/policies/enforcement/src/main/java/org/eclipse/ditto/policies/enforcement/AbstractEnforcerActor.java
@@ -28,6 +28,7 @@
 import org.eclipse.ditto.base.model.entity.id.EntityId;
 import org.eclipse.ditto.base.model.exceptions.DittoInternalErrorException;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
+import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.signals.Signal;
@@ -110,7 +111,7 @@ public Receive createReceive() {
                 .build();
     }
 
-    protected CompletionStage> loadPolicyEnforcer(Signal signal) {
+    protected CompletionStage> loadPolicyEnforcer(final Signal signal) {
         return providePolicyIdForEnforcement(signal)
                 .thenCompose(this::providePolicyEnforcer);
     }
@@ -129,10 +130,20 @@ private void enforceSignal(final S signal) {
     @SuppressWarnings("unchecked")
     private void doEnforceSignal(final S signal, final ActorRef sender) {
 
-        final var startedSpan = DittoTracing.newPreparedSpan(signal.getDittoHeaders(), SpanOperationName.of("enforce"))
+        final DittoHeaders dittoHeaders = signal.getDittoHeaders();
+        final var startedSpan = DittoTracing.newPreparedSpan(dittoHeaders,
+                        SpanOperationName.of("enforce_policy")
+                )
+                .correlationId(dittoHeaders.getCorrelationId().orElse(null))
                 .start();
-        final var tracedSignal =
-                signal.setDittoHeaders(DittoHeaders.of(startedSpan.propagateContext(signal.getDittoHeaders())));
+        final Optional formerTraceParent = dittoHeaders.getTraceParent();
+        final var tracedSignal = signal.setDittoHeaders(
+                DittoHeaders.of(startedSpan.propagateContext(
+                        dittoHeaders.toBuilder()
+                                .removeHeader(DittoHeaderDefinition.W3C_TRACEPARENT.getKey())
+                                .build()
+                ))
+        );
         final ActorRef self = getSelf();
 
         try {
@@ -150,13 +161,21 @@ private void doEnforceSignal(final S signal, final ActorRef sender) {
                                 }
                         );
                     })
+                    .thenCompose(this::performWotBasedSignalValidation)
                     .whenComplete((authorizedSignal, throwable) -> {
                         if (null != authorizedSignal) {
                             startedSpan.mark("enforce_success").finish();
                             log.withCorrelationId(authorizedSignal)
                                     .info("Completed enforcement of message type <{}> with outcome 'success'",
                                             authorizedSignal.getType());
-                            sender.tell(authorizedSignal, self);
+                            if (formerTraceParent.isPresent()) {
+                                sender.tell(authorizedSignal.setDittoHeaders(authorizedSignal.getDittoHeaders()
+                                        .toBuilder()
+                                        .traceparent(formerTraceParent.get())
+                                        .build()), self);
+                            } else {
+                                sender.tell(authorizedSignal, self);
+                            }
                         } else if (null != throwable) {
                             startedSpan.mark("enforce_failed").tagAsFailed(throwable).finish();
                             handleAuthorizationFailure(tracedSignal, throwable, sender);
@@ -174,6 +193,30 @@ private void doEnforceSignal(final S signal, final ActorRef sender) {
         }
     }
 
+    /**
+     * Performs an optional WoT based validation of the already {@code authorizedSignal}.
+     *
+     * @param authorizedSignal the signal to validate against a WoT model.
+     * @return a CompletionStage finished successfully with the {@code authorizedSignal} when WoT validation was
+     * either not applied or passed successfully. In case of a WoT validation error, exceptionally finished with
+     * a WoT validation exception.
+     */
+    protected CompletionStage performWotBasedSignalValidation(final S authorizedSignal) {
+        return CompletableFuture.completedStage(authorizedSignal);
+    }
+
+    /**
+     * Performs an optional WoT based validation of the already {@code filteredResponse}.
+     *
+     * @param filteredResponse the response to validate against a WoT model.
+     * @return a CompletionStage finished successfully with the {@code filteredResponse} when WoT validation was
+     * either not applied or passed successfully. In case of a WoT validation error, exceptionally finished with
+     * a WoT validation exception.
+     */
+    protected CompletionStage performWotBasedResponseValidation(final R filteredResponse) {
+        return CompletableFuture.completedStage(filteredResponse);
+    }
+
     private void handleAuthorizationFailure(
             final Signal signal,
             final Throwable throwable,
@@ -225,7 +268,7 @@ private CompletionStage filterResponse(final R commandResponse) {
                                 log.withCorrelationId(commandResponse)
                                         .debug("Could not filter command response because policyEnforcer was missing." +
                                                 " Likely the policy was deleted during command processing.");
-                                throw PolicyNotAccessibleException.newBuilder(pair.first()).build();
+                                return PolicyNotAccessibleException.newBuilder(pair.first()).build();
                             }))
                     .thenCompose(policyEnforcer -> doFilterResponse(commandResponse, policyEnforcer));
         } else {
@@ -236,7 +279,9 @@ private CompletionStage filterResponse(final R commandResponse) {
     private CompletionStage doFilterResponse(final R commandResponse, final PolicyEnforcer policyEnforcer) {
         try {
             final CompletionStage filteredResponseStage =
-                    enforcement.filterResponse(commandResponse, policyEnforcer);
+                    enforcement.filterResponse(commandResponse, policyEnforcer)
+                            .thenCompose(this::performWotBasedResponseValidation);
+
             return filteredResponseStage.handle((filteredResponse, throwable) -> {
                 if (null != filteredResponse) {
                     log.withCorrelationId(filteredResponse)
diff --git a/policies/enforcement/src/main/resources/reference.conf b/policies/enforcement/src/main/resources/reference.conf
index 6cc7c16513..1b623b87cd 100644
--- a/policies/enforcement/src/main/resources/reference.conf
+++ b/policies/enforcement/src/main/resources/reference.conf
@@ -3,12 +3,12 @@
 
 enforcement-cache-dispatcher {
   type = "Dispatcher"
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
 }
 
 enforcement-dispatcher {
   type = "Dispatcher"
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
 }
 
 ditto.extensions {
diff --git a/policies/service/pom.xml b/policies/service/pom.xml
index 5d009c872c..24e59c75af 100644
--- a/policies/service/pom.xml
+++ b/policies/service/pom.xml
@@ -140,19 +140,12 @@
             test
             test-jar
         
-        
-            org.eclipse.ditto
-            ditto-internal-utils-pekko
-            test
-            test-jar
-        
         
             org.eclipse.ditto
             ditto-base-model
             test-jar
             test
         
-
         
             org.eclipse.ditto
             ditto-base-service
diff --git a/policies/service/src/main/resources/policies.conf b/policies/service/src/main/resources/policies.conf
index da0d62dc87..43b35a2524 100755
--- a/policies/service/src/main/resources/policies.conf
+++ b/policies/service/src/main/resources/policies.conf
@@ -338,7 +338,7 @@ policy-journal-persistence-dispatcher {
   # which mailbox to use
   mailbox-type = "org.eclipse.ditto.policies.service.persistence.actors.PolicyPersistenceActorMailbox"
   mailbox-capacity = 100
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
   fork-join-executor {
     parallelism-min = 4
     parallelism-factor = 3.0
@@ -353,7 +353,7 @@ policy-snaps-persistence-dispatcher {
   # which mailbox to use
   mailbox-type = "org.eclipse.ditto.policies.service.persistence.actors.PolicyPersistenceActorMailbox"
   mailbox-capacity = 100
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
   fork-join-executor {
     parallelism-min = 4
     parallelism-factor = 3.0
@@ -365,7 +365,7 @@ policy-snaps-persistence-dispatcher {
 
 blocked-namespaces-dispatcher {
   type = Dispatcher
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
   fork-join-executor {
     # Min number of threads to cap factor-based parallelism number to
     parallelism-min = 4
diff --git a/pom.xml b/pom.xml
index 581b95557d..0f4d629559 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,10 +42,6 @@
         https://github.com/eclipse-ditto/ditto/issues
     
 
-    
-        3.5.0
-    
-
     
         ${release.scm.connection}
         ${release.scm.developerConnection}
diff --git a/things/service/pom.xml b/things/service/pom.xml
index 49013ab40c..0383a0c007 100644
--- a/things/service/pom.xml
+++ b/things/service/pom.xml
@@ -83,6 +83,10 @@
             org.eclipse.ditto
             ditto-wot-model
         
+        
+            org.eclipse.ditto
+            ditto-wot-validation
+        
         
             org.eclipse.ditto
             ditto-wot-integration
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/DittoThingsConfig.java b/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/DittoThingsConfig.java
index 7570d5bcc6..611ea6ee93 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/DittoThingsConfig.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/DittoThingsConfig.java
@@ -30,8 +30,8 @@
 import org.eclipse.ditto.internal.utils.persistence.operations.DefaultPersistenceOperationsConfig;
 import org.eclipse.ditto.internal.utils.persistence.operations.PersistenceOperationsConfig;
 import org.eclipse.ditto.internal.utils.tracing.config.TracingConfig;
-import org.eclipse.ditto.wot.integration.config.DefaultWotConfig;
-import org.eclipse.ditto.wot.integration.config.WotConfig;
+import org.eclipse.ditto.wot.api.config.DefaultWotConfig;
+import org.eclipse.ditto.wot.api.config.WotConfig;
 
 /**
  * This class implements the config of the Ditto Things service.
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/ThingsConfig.java b/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/ThingsConfig.java
index 0e26ac4b46..327e942eeb 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/ThingsConfig.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/common/config/ThingsConfig.java
@@ -19,7 +19,7 @@
 import org.eclipse.ditto.internal.utils.health.config.WithHealthCheckConfig;
 import org.eclipse.ditto.internal.utils.persistence.mongo.config.WithMongoDbConfig;
 import org.eclipse.ditto.internal.utils.persistence.operations.WithPersistenceOperationsConfig;
-import org.eclipse.ditto.wot.integration.config.WotConfig;
+import org.eclipse.ditto.wot.api.config.WotConfig;
 
 /**
  * Provides the configuration settings of the Things service.
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcement.java b/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcement.java
index cb82581cf9..7e21214c3a 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcement.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/enforcement/ThingEnforcement.java
@@ -15,6 +15,8 @@
 import java.util.List;
 import java.util.concurrent.CompletionStage;
 
+import org.apache.pekko.actor.ActorRef;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.signals.Signal;
 import org.eclipse.ditto.base.model.signals.commands.CommandResponse;
 import org.eclipse.ditto.policies.enforcement.AbstractEnforcementReloaded;
@@ -24,9 +26,6 @@
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.signals.commands.modify.CreateThing;
 
-import org.apache.pekko.actor.ActorRef;
-import org.apache.pekko.actor.ActorSystem;
-
 /**
  * Authorizes {@link Signal}s and filters {@link CommandResponse}s related to things by applying different included
  * {@link ThingEnforcementStrategy}s.
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 8a24dec6b5..33f8db3394 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
@@ -18,12 +18,14 @@
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.function.Function;
 
 import javax.annotation.Nullable;
 
 import org.apache.pekko.actor.ActorRef;
 import org.apache.pekko.actor.ActorSystem;
 import org.apache.pekko.actor.Props;
+import org.apache.pekko.japi.Pair;
 import org.apache.pekko.pattern.AskTimeoutException;
 import org.apache.pekko.pattern.Patterns;
 import org.eclipse.ditto.base.model.auth.AuthorizationSubject;
@@ -32,15 +34,28 @@
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
+import org.eclipse.ditto.base.model.headers.contenttype.ContentType;
 import org.eclipse.ditto.base.model.namespaces.NamespaceBlockedException;
 import org.eclipse.ditto.base.model.signals.Signal;
 import org.eclipse.ditto.base.model.signals.commands.CommandResponse;
 import org.eclipse.ditto.internal.utils.cacheloaders.AskWithRetry;
 import org.eclipse.ditto.internal.utils.cacheloaders.config.AskWithRetryConfig;
+import org.eclipse.ditto.internal.utils.tracing.DittoTracing;
+import org.eclipse.ditto.internal.utils.tracing.span.SpanOperationName;
+import org.eclipse.ditto.internal.utils.tracing.span.StartedSpan;
 import org.eclipse.ditto.json.JsonFieldSelector;
 import org.eclipse.ditto.json.JsonObject;
 import org.eclipse.ditto.json.JsonObjectBuilder;
 import org.eclipse.ditto.json.JsonRuntimeException;
+import org.eclipse.ditto.json.JsonValue;
+import org.eclipse.ditto.messages.model.Message;
+import org.eclipse.ditto.messages.model.MessageDirection;
+import org.eclipse.ditto.messages.model.signals.commands.MessageCommand;
+import org.eclipse.ditto.messages.model.signals.commands.MessageCommandResponse;
+import org.eclipse.ditto.messages.model.signals.commands.SendFeatureMessage;
+import org.eclipse.ditto.messages.model.signals.commands.SendFeatureMessageResponse;
+import org.eclipse.ditto.messages.model.signals.commands.SendThingMessage;
+import org.eclipse.ditto.messages.model.signals.commands.SendThingMessageResponse;
 import org.eclipse.ditto.policies.api.PoliciesValidator;
 import org.eclipse.ditto.policies.api.PolicyTag;
 import org.eclipse.ditto.policies.enforcement.AbstractEnforcementReloaded;
@@ -63,7 +78,10 @@
 import org.eclipse.ditto.policies.model.signals.commands.query.RetrievePolicyResponse;
 import org.eclipse.ditto.things.api.commands.sudo.SudoRetrieveThing;
 import org.eclipse.ditto.things.api.commands.sudo.SudoRetrieveThingResponse;
+import org.eclipse.ditto.things.model.Feature;
+import org.eclipse.ditto.things.model.FeatureDefinition;
 import org.eclipse.ditto.things.model.Thing;
+import org.eclipse.ditto.things.model.ThingDefinition;
 import org.eclipse.ditto.things.model.ThingId;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommand;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandResponse;
@@ -73,6 +91,8 @@
 import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingNotModifiableException;
 import org.eclipse.ditto.things.model.signals.commands.modify.CreateThing;
 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;
 
 /**
  * Enforcer responsible for enforcing {@link ThingCommand}s and filtering {@link ThingCommandResponse}s utilizing the
@@ -82,13 +102,16 @@ public final class ThingEnforcerActor
         extends AbstractPolicyLoadingEnforcerActor, CommandResponse, ThingEnforcement> {
 
     private static final String ENFORCEMENT_DISPATCHER = "enforcement-dispatcher";
+
     /**
      * Label of default policy entry in default policy.
      */
     private static final String DEFAULT_POLICY_ENTRY_LABEL = "DEFAULT";
+
     private final PolicyIdReferencePlaceholderResolver policyIdReferencePlaceholderResolver;
     private final ActorRef policiesShardRegion;
     private final AskWithRetryConfig askWithRetryConfig;
+    private final WotThingModelValidator thingModelValidator;
 
     @SuppressWarnings("unused")
     private ThingEnforcerActor(final ThingId thingId,
@@ -105,6 +128,9 @@ private ThingEnforcerActor(final ThingId thingId,
         final ActorSystem system = context().system();
         policyIdReferencePlaceholderResolver = PolicyIdReferencePlaceholderResolver.of(
                 thingsShardRegion, askWithRetryConfig, system);
+
+        final DittoWotIntegration wotIntegration = DittoWotIntegration.get(system);
+        thingModelValidator = wotIntegration.getWotThingModelValidator();
     }
 
     /**
@@ -152,7 +178,7 @@ private CompletionStage getDreForMissingPolicyEnforcer(fi
             final PolicyId policyId) {
 
         return doesThingExist().thenApply(thingExists -> {
-            if (thingExists) {
+            if (Boolean.TRUE.equals(thingExists)) {
                 return errorForExistingThingWithDeletedPolicy(thingCommand, policyId);
             } else {
                 return ThingNotAccessibleException.newBuilder(entityId)
@@ -162,6 +188,69 @@ private CompletionStage getDreForMissingPolicyEnforcer(fi
         });
     }
 
+    @Override
+    protected CompletionStage> performWotBasedSignalValidation(final Signal authorizedSignal
+    ) {
+        if (authorizedSignal instanceof MessageCommand messageCommand) {
+            final var startedSpan = DittoTracing.newPreparedSpan(
+                            messageCommand.getDittoHeaders(),
+                            SpanOperationName.of("enforce_wot_model message")
+                    )
+                    .start();
+            return performWotBasedMessageCommandValidation(messageCommand.setDittoHeaders(
+                    DittoHeaders.of(startedSpan.propagateContext(messageCommand.getDittoHeaders())))
+            ).whenComplete((result, error) -> {
+                        if (error instanceof DittoRuntimeException dre) {
+                            startedSpan.tagAsFailed(dre.toString());
+                        } else if (null != error) {
+                            startedSpan.tagAsFailed(error);
+                        }
+                        startedSpan.finish();
+                    })
+                    .thenApply(Function.identity());
+        } else if (authorizedSignal instanceof MessageCommandResponse messageCommandResponse) {
+            return doPerformWotBasedMessageCommandResponseValidation(messageCommandResponse)
+                    .thenApply(Function.identity());
+        } else {
+            return super.performWotBasedSignalValidation(authorizedSignal);
+        }
+    }
+
+    @Override
+    protected CompletionStage> performWotBasedResponseValidation(
+            final CommandResponse filteredResponse
+    ) {
+        if (filteredResponse instanceof MessageCommandResponse messageCommandResponse) {
+            return doPerformWotBasedMessageCommandResponseValidation(messageCommandResponse)
+                    .thenApply(Function.identity());
+        } else {
+            return super.performWotBasedResponseValidation(filteredResponse);
+        }
+    }
+
+    private CompletionStage> doPerformWotBasedMessageCommandResponseValidation(
+            final MessageCommandResponse messageCommandResponse
+    ) {
+        final var startedSpan = DittoTracing.newPreparedSpan(
+                        messageCommandResponse.getDittoHeaders(),
+                        SpanOperationName.of("enforce_wot_model message_response")
+                )
+                .start();
+        return performWotBasedMessageCommandResponseValidation(
+                messageCommandResponse.setDittoHeaders(
+                        DittoHeaders.of(startedSpan.propagateContext(messageCommandResponse.getDittoHeaders()))
+                )
+        )
+                .whenComplete((result, error) -> {
+                    if (error instanceof DittoRuntimeException dre) {
+                        startedSpan.tagAsFailed(dre.toString());
+                    } else if (null != error) {
+                        startedSpan.tagAsFailed(error);
+                    }
+                    startedSpan.finish();
+                });
+    }
+
     /**
      * Create error for commands to an existing thing whose policy is deleted.
      *
@@ -331,7 +420,7 @@ private static Policy getDefaultPolicy(final DittoHeaders dittoHeaders, final Th
                 .orElseThrow(() -> {
                     final var message = String.format("The Thing with ID '%s' could not be created with " +
                             "implicit Policy because no authorization subject is present.", thingId);
-                    throw ThingNotCreatableException.newBuilderForPolicyMissing(thingId, PolicyId.of(thingId))
+                    return ThingNotCreatableException.newBuilderForPolicyMissing(thingId, PolicyId.of(thingId))
                             .message(message)
                             .description(() -> null)
                             .dittoHeaders(dittoHeaders)
@@ -411,8 +500,8 @@ private ThingNotCreatableException reportInitialPolicyCreationFailure(final Poli
 
         log.withCorrelationId(command)
                 .info("Failed to create Policy with ID <{}> due to: <{}: {}>." +
-                        " The CreateThing command which would have created a Policy for the Thing with ID <{}>" +
-                        " is therefore not handled.", policyId,
+                                " The CreateThing command which would have created a Policy for the Thing with ID <{}>" +
+                                " is therefore not handled.", policyId,
                         policyException.getClass().getSimpleName(), policyException.getMessage(),
                         command.getEntityId()
                 );
@@ -425,7 +514,8 @@ private ThingNotCreatableException reportInitialPolicyCreationFailure(final Poli
                     .dittoHeaders(command.getDittoHeaders())
                     .build();
         } else {
-            return ThingNotCreatableException.newBuilderForOtherReason(policyException.getHttpStatus(), command.getEntityId(), policyId,
+            return ThingNotCreatableException.newBuilderForOtherReason(policyException.getHttpStatus(),
+                            command.getEntityId(), policyId,
                             policyException.getMessage())
                     .dittoHeaders(command.getDittoHeaders())
                     .build();
@@ -434,15 +524,23 @@ private ThingNotCreatableException reportInitialPolicyCreationFailure(final Poli
 
     @Override
     protected CompletionStage providePolicyIdForEnforcement(final Signal signal) {
-        return Patterns.ask(getContext().getParent(), SudoRetrieveThing.of(entityId,
-                        JsonFieldSelector.newInstance("policyId"),
-                        DittoHeaders.newBuilder()
-                                .correlationId("sudoRetrieveThingFromThingEnforcerActor-" + UUID.randomUUID())
-                                .putHeader(DittoHeaderDefinition.DITTO_RETRIEVE_DELETED.getKey(),
-                                        Boolean.TRUE.toString())
-                                .build()
-                ), DEFAULT_LOCAL_ASK_TIMEOUT
-        ).thenApply(response -> extractPolicyIdFromSudoRetrieveThingResponse(response).orElse(null));
+        final var startedSpan = DittoTracing.newPreparedSpan(
+                        signal.getDittoHeaders(),
+                        SpanOperationName.of("sudo_retrieve_thing")
+                )
+                .correlationId(signal.getDittoHeaders().getCorrelationId().orElse(null))
+                .start();
+        final SudoRetrieveThing sudoRetrieveThing = SudoRetrieveThing.of(entityId,
+                JsonFieldSelector.newInstance("policyId"),
+                DittoHeaders.of(startedSpan.propagateContext(DittoHeaders.newBuilder()
+                        .correlationId("sudoRetrieveThingFromThingEnforcerActor-" + UUID.randomUUID())
+                        .putHeader(DittoHeaderDefinition.DITTO_RETRIEVE_DELETED.getKey(),
+                                Boolean.TRUE.toString())
+                        .build()))
+        );
+        return Patterns.ask(getContext().getParent(), sudoRetrieveThing, DEFAULT_LOCAL_ASK_TIMEOUT)
+                .thenApply(
+                        response -> extractPolicyIdFromSudoRetrieveThingResponse(response, startedSpan).orElse(null));
     }
 
     private CompletionStage doesThingExist() {
@@ -463,20 +561,225 @@ 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);
+            }
+        } else {
+            return CompletableFuture.completedFuture(messageCommand);
+        }
+    }
+
+    private CompletionStage performWotBasedThingMessageValidation(final MessageCommand messageCommand,
+            final SendThingMessage sendThingMessage,
+            final MessageDirection messageDirection,
+            @Nullable final JsonValue messageCommandPayload
+    ) {
+        return resolveThingDefinition()
+                .thenCompose(optThingDefinition -> {
+                    if (messageDirection == MessageDirection.TO) {
+                        return thingModelValidator.validateThingActionInput(
+                                optThingDefinition.orElse(null),
+                                sendThingMessage.getMessage().getSubject(),
+                                messageCommandPayload,
+                                sendThingMessage.getResourcePath(),
+                                sendThingMessage.getDittoHeaders()
+                        );
+                    } else if (messageDirection == MessageDirection.FROM) {
+                        return thingModelValidator.validateThingEventData(
+                                optThingDefinition.orElse(null),
+                                sendThingMessage.getMessage().getSubject(),
+                                messageCommandPayload,
+                                sendThingMessage.getResourcePath(),
+                                sendThingMessage.getDittoHeaders()
+                        );
+                    } else {
+                        return CompletableFuture.failedStage(DittoInternalErrorException.newBuilder()
+                                .message("Unknown message direction")
+                                .dittoHeaders(messageCommand.getDittoHeaders())
+                                .build()
+                        );
+                    }
+                });
+    }
+
+    private CompletionStage performWotBasedFeatureMessageValidation(final MessageCommand messageCommand,
+            final SendFeatureMessage sendFeatureMessage,
+            final String featureId,
+            final MessageDirection messageDirection,
+            @Nullable final JsonValue messageCommandPayload
+    ) {
+        return resolveThingAndFeatureDefinition(featureId)
+                .thenCompose(optDefinitionPair -> {
+                    if (messageDirection == MessageDirection.TO) {
+                        return thingModelValidator.validateFeatureActionInput(
+                                optDefinitionPair.first().orElse(null),
+                                optDefinitionPair.second().orElse(null),
+                                featureId,
+                                sendFeatureMessage.getMessage().getSubject(),
+                                messageCommandPayload,
+                                sendFeatureMessage.getResourcePath(),
+                                sendFeatureMessage.getDittoHeaders()
+                        );
+                    } else if (messageDirection == MessageDirection.FROM) {
+                        return thingModelValidator.validateFeatureEventData(
+                                optDefinitionPair.first().orElse(null),
+                                optDefinitionPair.second().orElse(null),
+                                featureId,
+                                sendFeatureMessage.getMessage().getSubject(),
+                                messageCommandPayload,
+                                sendFeatureMessage.getResourcePath(),
+                                sendFeatureMessage.getDittoHeaders()
+                        );
+                    } else {
+                        return CompletableFuture.failedStage(DittoInternalErrorException.newBuilder()
+                                .message("Unknown message direction")
+                                .dittoHeaders(messageCommand.getDittoHeaders())
+                                .build()
+                        );
+                    }
+                });
+    }
+
+    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);
+            }
+        } else {
+            return CompletableFuture.completedFuture(messageCommandResponse);
+        }
+    }
+
+    private static boolean isJsonMessageContent(final Message message) {
+        return message
+                .getInterpretedContentType()
+                .filter(ContentType::isJson)
+                .isPresent();
+    }
+
+    private CompletionStage> resolveThingDefinition() {
+        return Patterns.ask(getContext().getParent(), SudoRetrieveThing.of(entityId,
+                        JsonFieldSelector.newInstance("definition"),
+                        DittoHeaders.newBuilder()
+                                .correlationId("sudoRetrieveThingDefinitionFromThingEnforcerActor-" + UUID.randomUUID())
+                                .build()
+                ), DEFAULT_LOCAL_ASK_TIMEOUT
+        ).thenApply(response -> {
+            if (response instanceof SudoRetrieveThingResponse sudoRetrieveThingResponse) {
+                return sudoRetrieveThingResponse.getThing().getDefinition();
+            } else if (response instanceof ThingNotAccessibleException) {
+                return Optional.empty();
+            } else {
+                throw new IllegalStateException("expected SudoRetrieveThingResponse, got: " + response);
+            }
+        });
+    }
+
+    private CompletionStage, Optional>>
+    resolveThingAndFeatureDefinition(final String featureId) {
+        return Patterns.ask(getContext().getParent(), SudoRetrieveThing.of(entityId,
+                        JsonFieldSelector.newInstance("definition", "features/" + featureId + "/definition"),
+                        DittoHeaders.newBuilder()
+                                .correlationId(
+                                        "sudoRetrieveThingAndFeatureDefinitionFromThingEnforcerActor-" + UUID.randomUUID())
+                                .build()
+                ), DEFAULT_LOCAL_ASK_TIMEOUT
+        ).thenApply(response -> {
+            if (response instanceof SudoRetrieveThingResponse sudoRetrieveThingResponse) {
+                return new Pair<>(sudoRetrieveThingResponse.getThing().getDefinition(),
+                        sudoRetrieveThingResponse.getThing()
+                                .getFeatures()
+                                .flatMap(f -> f.getFeature(featureId))
+                                .flatMap(Feature::getDefinition)
+                );
+            } else if (response instanceof ThingNotAccessibleException) {
+                return new Pair<>(Optional.empty(), Optional.empty());
+            } else {
+                throw new IllegalStateException("expected SudoRetrieveThingResponse, got: " + response);
+            }
+        });
+    }
+
     /**
      * Extracts a {@link PolicyId} from the passed {@code response} which is expected to be a
      * {@link SudoRetrieveThingResponse}. A {@code response} being a {@link ThingNotAccessibleException} leads to an
      * empty Optional.
      *
      * @param response the response to extract the PolicyId from.
+     * @param startedSpan the started span to finish upon success or failure
      * @return the optional extracted PolicyId.
      */
-    static Optional extractPolicyIdFromSudoRetrieveThingResponse(final Object response) {
+    static Optional extractPolicyIdFromSudoRetrieveThingResponse(final Object response,
+            final StartedSpan startedSpan) {
         if (response instanceof SudoRetrieveThingResponse sudoRetrieveThingResponse) {
+            startedSpan.finish();
             return sudoRetrieveThingResponse.getThing().getPolicyId();
         } else if (response instanceof ThingNotAccessibleException) {
+            startedSpan.tagAsFailed("Thing not accessible")
+                    .finish();
             return Optional.empty();
         } else {
+            startedSpan.tagAsFailed("expected SudoRetrieveThingResponse, got: " + response)
+                    .finish();
             throw new IllegalStateException("expected SudoRetrieveThingResponse, got: " + response);
         }
     }
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingCommandStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingCommandStrategy.java
index ea04c8b4f0..67360f2d98 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingCommandStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingCommandStrategy.java
@@ -21,6 +21,7 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.entity.metadata.MetadataBuilder;
 import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory;
@@ -45,6 +46,10 @@
 import org.eclipse.ditto.things.model.signals.commands.modify.ThingModifyCommand;
 import org.eclipse.ditto.things.model.signals.commands.query.ThingQueryCommand;
 import org.eclipse.ditto.things.model.signals.events.ThingEvent;
+import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator;
+import org.eclipse.ditto.wot.api.generator.WotThingSkeletonGenerator;
+import org.eclipse.ditto.wot.api.validator.WotThingModelValidator;
+import org.eclipse.ditto.wot.integration.DittoWotIntegration;
 
 /**
  * Abstract base class for {@link org.eclipse.ditto.things.model.signals.commands.ThingCommand} strategies.
@@ -59,8 +64,16 @@ abstract class AbstractThingCommandStrategy>
     private static final ConditionalHeadersValidator VALIDATOR =
             ThingsConditionalHeadersValidatorProvider.getInstance();
 
-    protected AbstractThingCommandStrategy(final Class theMatchingClass) {
+    protected final WotThingDescriptionGenerator wotThingDescriptionGenerator;
+    protected final WotThingSkeletonGenerator wotThingSkeletonGenerator;
+    protected final WotThingModelValidator wotThingModelValidator;
+
+    protected AbstractThingCommandStrategy(final Class theMatchingClass, final ActorSystem actorSystem) {
         super(theMatchingClass);
+        final DittoWotIntegration wotIntegration = DittoWotIntegration.get(actorSystem);
+        wotThingDescriptionGenerator = wotIntegration.getWotThingDescriptionGenerator();
+        wotThingSkeletonGenerator = wotIntegration.getWotThingSkeletonGenerator();
+        wotThingModelValidator = wotIntegration.getWotThingModelValidator();
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingModifyCommandStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingModifyCommandStrategy.java
new file mode 100644
index 0000000000..e35b47dfe8
--- /dev/null
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractThingModifyCommandStrategy.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2024 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.things.service.persistence.actors.strategies.commands;
+
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import org.apache.pekko.actor.ActorSystem;
+import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
+import org.eclipse.ditto.base.model.headers.DittoHeaders;
+import org.eclipse.ditto.internal.utils.tracing.DittoTracing;
+import org.eclipse.ditto.internal.utils.tracing.span.SpanOperationName;
+import org.eclipse.ditto.things.model.Thing;
+import org.eclipse.ditto.things.model.signals.commands.modify.ThingModifyCommand;
+
+/**
+ * Abstract base class for {@link ThingModifyCommand} strategies.
+ *
+ * @param  the type of the handled command - of type {@code ThingModifyCommand}
+ */
+@Immutable
+abstract class AbstractThingModifyCommandStrategy>
+        extends AbstractThingCommandStrategy {
+
+    protected AbstractThingModifyCommandStrategy(final Class theMatchingClass, final ActorSystem actorSystem) {
+        super(theMatchingClass, actorSystem);
+    }
+
+    /**
+     * Builds a CompletionStage which asynchronously validates the passed in {@code command} and the {@code thing},
+     * using the {@link #performWotValidation(ThingModifyCommand, Thing, Thing)} abstract method of this class.
+     *
+     * @param command the command to validate
+     * @param thing the (previous) thing state to use for obtaining validation information
+     * @return a CompletionStage which asynchronously validates the passed in {@code command} and fails with a
+     * {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} if the command could not be
+     * validated successfully
+     */
+    protected CompletionStage buildValidatedStage(final C command, @Nullable final Thing thing) {
+        return buildValidatedStage(command, thing, null);
+    }
+
+    /**
+     * Builds a CompletionStage which asynchronously validates the passed in {@code command} and the {@code previousThing},
+     * using the {@link #performWotValidation(ThingModifyCommand, Thing, Thing)} abstract method of this class.
+     *
+     * @param command the command to validate
+     * @param previousThing the (previous) thing state to use for obtaining validation information
+     * @param previewThing the thing state as preview how it would look like having applied the command (e.g. after merge)
+     * @return a CompletionStage which asynchronously validates the passed in {@code command} and fails with a
+     * {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} if the command could not be
+     * validated successfully
+     */
+    protected CompletionStage buildValidatedStage(final C command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        final var startedSpan = DittoTracing.newPreparedSpan(
+                        command.getDittoHeaders(),
+                        SpanOperationName.of("enforce_wot_model")
+                )
+                .correlationId(command.getDittoHeaders().getCorrelationId().orElse(null))
+                .start();
+        final var tracedCommand =
+                command.setDittoHeaders(DittoHeaders.of(startedSpan.propagateContext(command.getDittoHeaders())));
+        return performWotValidation(tracedCommand, previousThing, previewThing)
+                .whenComplete((result, throwable) -> {
+                    if (throwable instanceof CompletionException completionException) {
+                        if (completionException.getCause() instanceof DittoRuntimeException dre) {
+                            startedSpan.tagAsFailed(dre.toString())
+                                    .finish();
+                        } else {
+                            startedSpan.tagAsFailed(
+                                    completionException.getCause().getClass().getSimpleName() + ": " +
+                                            throwable.getCause().getMessage()
+                                    ).finish();
+                        }
+                        return;
+                    }
+
+                    if (throwable != null) {
+                        startedSpan.tagAsFailed(throwable.getClass().getSimpleName() + ": " + throwable.getMessage())
+                                .finish();
+                    } else {
+                        startedSpan.finish();
+                    }
+                });
+
+    }
+
+    /**
+     * Performs WoT based validation of the passed {@code command} and {@code thing}, depending on the concrete command
+     * strategy.
+     *
+     * @param command the command to validate
+     * @param previousThing the (previous) thing state to use for obtaining validation information
+     * @param previewThing the thing state as preview how it would look like having applied the command (e.g. after merge)
+     * @return a CompletionStage which asynchronously validates the passed in {@code command} and fails with a
+     * {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} if the command could not be
+     * validated successfully
+     */
+    protected abstract CompletionStage performWotValidation(C command,
+            @Nullable Thing previousThing,
+            @Nullable Thing previewThing
+    );
+}
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/CreateThingStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/CreateThingStrategy.java
index 7f34c850ca..9e4b20358c 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/CreateThingStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/CreateThingStrategy.java
@@ -12,9 +12,6 @@
  */
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
-import static org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory.newErrorResult;
-import static org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory.newMutationResult;
-
 import java.time.Instant;
 import java.util.Objects;
 import java.util.Optional;
@@ -23,13 +20,15 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
+import org.apache.pekko.japi.Pair;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
-import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
 import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.json.JsonFactory;
 import org.eclipse.ditto.json.JsonValue;
 import org.eclipse.ditto.policies.model.PolicyId;
@@ -42,17 +41,12 @@
 import org.eclipse.ditto.things.model.signals.commands.modify.CreateThingResponse;
 import org.eclipse.ditto.things.model.signals.events.ThingCreated;
 import org.eclipse.ditto.things.model.signals.events.ThingEvent;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
-
-import org.apache.pekko.actor.ActorSystem;
 
 /**
  * This strategy handles the {@link CreateThingStrategy} command.
  */
 @Immutable
-final class CreateThingStrategy extends AbstractThingCommandStrategy {
-
-    private final WotThingDescriptionProvider wotThingDescriptionProvider;
+final class CreateThingStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@link CreateThingStrategy} object.
@@ -60,8 +54,7 @@ final class CreateThingStrategy extends AbstractThingCommandStrategy> doApply(final Context context,
         // Thing not yet created - do so ..
         Thing newThing;
         try {
-            newThing =
-                    handleCommandVersion(context, command.getImplementedSchemaVersion(), command.getThing(),
-                            commandHeaders);
+            newThing = handleCommandVersion(context, command.getThing(), commandHeaders);
         } catch (final DittoRuntimeException e) {
-            return newErrorResult(e, command);
+            return ResultFactory.newErrorResult(e, command);
         }
 
         // for v2 upwards, set the policy-id to the thing-id if none is specified:
@@ -110,7 +101,7 @@ protected Result> doApply(final Context context,
         final Instant now = Instant.now();
 
         final Thing finalNewThing = newThing;
-        final CompletionStage thingStage = wotThingDescriptionProvider.provideThingSkeletonForCreation(
+        final CompletionStage thingStage = wotThingSkeletonGenerator.provideThingSkeletonForCreation(
                         command.getEntityId(),
                         newThing.getDefinition().orElse(null),
                         commandHeaders
@@ -130,23 +121,41 @@ protected Result> doApply(final Context context,
                         .build()
                 );
 
-        final CompletionStage> eventStage =
-                thingStage.thenApply(newThingWithImplicits ->
-                        ThingCreated.of(newThingWithImplicits, nextRevision, now, commandHeaders, metadata)
+        // validate based on potentially referenced Thing WoT TM/TD
+        final CompletionStage> validatedStage =
+                thingStage.thenCompose(createdThingWithImplicits ->
+                        buildValidatedStage(command, null, createdThingWithImplicits)
+                                .thenApply(createThing -> new Pair<>(createThing, createdThingWithImplicits))
                 );
 
-        final CompletionStage responseStage = thingStage.thenApply(newThingWithImplicits ->
-                appendETagHeaderIfProvided(command, CreateThingResponse.of(newThingWithImplicits, commandHeaders),
-                        newThingWithImplicits)
+        final CompletionStage> eventStage = validatedStage.thenApply(pair ->
+                ThingCreated.of(pair.second(), nextRevision, now, commandHeaders, metadata)
         );
 
-        return newMutationResult(command, eventStage, responseStage, true, false);
+        final CompletionStage responseStage = validatedStage.thenApply(pair ->
+                appendETagHeaderIfProvided(pair.first(), CreateThingResponse.of(pair.second(), commandHeaders),
+                        pair.second())
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage, true, false);
     }
 
-    private Thing handleCommandVersion(final Context context, final JsonSchemaVersion version,
-            final Thing thing,
-            final DittoHeaders dittoHeaders) {
+    @Override
+    protected CompletionStage performWotValidation(final CreateThing command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThing(
+                Optional.ofNullable(previewThing).orElse(command.getThing()),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
 
+    private Thing handleCommandVersion(final Context context,
+            final Thing thing,
+            final DittoHeaders dittoHeaders
+    ) {
         // policyId is required for v2
         if (thing.getPolicyId().isEmpty()) {
             throw PolicyIdMissingException.fromThingIdOnCreate(context.getState(), dittoHeaders);
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategy.java
index 8a1f6d9891..87ae1b5cbb 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategy.java
@@ -13,20 +13,22 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonPointer;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonPointer;
 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.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteAttribute;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteAttributeResponse;
 import org.eclipse.ditto.things.model.signals.events.AttributeDeleted;
@@ -36,13 +38,15 @@
  * This strategy handles the {@link DeleteAttribute} command.
  */
 @Immutable
-final class DeleteAttributeStrategy extends AbstractThingCommandStrategy {
+final class DeleteAttributeStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteAttributeStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteAttributeStrategy() {
-        super(DeleteAttribute.class);
+    DeleteAttributeStrategy(final ActorSystem actorSystem) {
+        super(DeleteAttribute.class, actorSystem);
     }
 
     @Override
@@ -63,17 +67,36 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final DeleteAttribute command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Result> getDeleteAttributeResult(final Context context, final long nextRevision,
             final DeleteAttribute command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
         final ThingId thingId = context.getState();
         final JsonPointer attrPointer = command.getAttributePointer();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                DeleteAttributeResponse.of(thingId, attrPointer, dittoHeaders), thing);
 
-        return ResultFactory.newMutationResult(command,
-                AttributeDeleted.of(thingId, attrPointer, nextRevision, getEventTimestamp(), dittoHeaders, metadata),
-                response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(deleteAttribute ->
+                AttributeDeleted.of(thingId, attrPointer, nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(deleteAttribute ->
+                appendETagHeaderIfProvided(deleteAttribute,
+                        DeleteAttributeResponse.of(thingId, attrPointer, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
 
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategy.java
index 9561f4257d..9cce472be2 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 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.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteAttributes;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteAttributesResponse;
 import org.eclipse.ditto.things.model.signals.events.AttributesDeleted;
@@ -35,14 +37,15 @@
  * This strategy handles the {@link DeleteAttributes} command.
  */
 @Immutable
-final class DeleteAttributesStrategy
-        extends AbstractThingCommandStrategy {
+final class DeleteAttributesStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteAttributesStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteAttributesStrategy() {
-        super(DeleteAttributes.class);
+    DeleteAttributesStrategy(final ActorSystem actorSystem) {
+        super(DeleteAttributes.class, actorSystem);
     }
 
     @Override
@@ -58,6 +61,20 @@ protected Result> doApply(final Context context,
                         ExceptionFactory.attributesNotFound(context.getState(), command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final DeleteAttributes command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractAttributes(final @Nullable Thing thing) {
         return getEntityOrThrow(thing).getAttributes();
     }
@@ -67,11 +84,15 @@ private Result> getDeleteAttributesResult(final Context c
         final ThingId thingId = context.getState();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                DeleteAttributesResponse.of(thingId, dittoHeaders), thing);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(deleteAttributes ->
+                AttributesDeleted.of(thingId, nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(deleteAttributes ->
+                appendETagHeaderIfProvided(deleteAttributes, DeleteAttributesResponse.of(thingId, dittoHeaders), thing)
+        );
 
-        return ResultFactory.newMutationResult(command,
-                AttributesDeleted.of(thingId, nextRevision, getEventTimestamp(), dittoHeaders, metadata), response);
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategy.java
index 71b5f2b941..8a88146b01 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDefinition;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDefinitionResponse;
 import org.eclipse.ditto.things.model.signals.events.FeatureDefinitionDeleted;
@@ -35,13 +37,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDefinition} command.
  */
 @Immutable
-final class DeleteFeatureDefinitionStrategy extends AbstractThingCommandStrategy {
+final class DeleteFeatureDefinitionStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeatureDefinitionStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteFeatureDefinitionStrategy() {
-        super(DeleteFeatureDefinition.class);
+    DeleteFeatureDefinitionStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeatureDefinition.class, actorSystem);
     }
 
     @Override
@@ -60,10 +64,32 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final DeleteFeatureDefinition command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureDefinitionDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElseThrow(),
+                command.getFeatureId(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Result> getDeleteFeatureDefinitionResult(final Feature feature,
             final Context context,
-            final long nextRevision, final DeleteFeatureDefinition command, @Nullable final Thing thing,
-            @Nullable final Metadata metadata) {
+            final long nextRevision,
+            final DeleteFeatureDefinition command,
+            @Nullable final Thing thing,
+            @Nullable final Metadata metadata
+    ) {
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
         final ThingId thingId = context.getState();
@@ -71,12 +97,18 @@ private Result> getDeleteFeatureDefinitionResult(final Feature fea
 
         return feature.getDefinition()
                 .map(featureDefinition -> {
-                    final ThingEvent event =
-                            FeatureDefinitionDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(),
-                                    dittoHeaders, metadata);
-                    final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                            DeleteFeatureDefinitionResponse.of(thingId, featureId, dittoHeaders), thing);
-                    return ResultFactory.>newMutationResult(command, event, response);
+                    final CompletionStage validatedStage = buildValidatedStage(command, thing);
+                    final CompletionStage> eventStage =
+                            validatedStage.thenApply(deleteFeatureDefinition ->
+                                    FeatureDefinitionDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(),
+                                            dittoHeaders, metadata)
+                            );
+                    final CompletionStage responseStage =
+                            validatedStage.thenApply(deleteFeatureDefinition ->
+                                    appendETagHeaderIfProvided(deleteFeatureDefinition,
+                                            DeleteFeatureDefinitionResponse.of(thingId, featureId, dittoHeaders), thing)
+                            );
+                    return ResultFactory.newMutationResult(command, eventStage, responseStage);
                 })
                 .orElseGet(() -> ResultFactory.newErrorResult(
                         ExceptionFactory.featureDefinitionNotFound(thingId, featureId, dittoHeaders), command));
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategy.java
index 3ed7f63996..5e4c7013b4 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDesiredProperties;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDesiredPropertiesResponse;
 import org.eclipse.ditto.things.model.signals.events.FeatureDesiredPropertiesDeleted;
@@ -36,13 +38,13 @@
  */
 @Immutable
 final class DeleteFeatureDesiredPropertiesStrategy
-        extends AbstractThingCommandStrategy {
+        extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeatureDesiredPropertiesStrategy} object.
      */
-    DeleteFeatureDesiredPropertiesStrategy() {
-        super(DeleteFeatureDesiredProperties.class);
+    DeleteFeatureDesiredPropertiesStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeatureDesiredProperties.class, actorSystem);
     }
 
     @Override
@@ -60,6 +62,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final DeleteFeatureDesiredProperties command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final DeleteFeatureDesiredProperties command,
             final @Nullable Thing thing) {
 
@@ -80,12 +103,22 @@ private Result> getDeleteFeatureDesiredPropertiesResult(final Feat
 
         return feature.getDesiredProperties()
                 .map(desiredProperties -> {
-                    final ThingEvent event =
-                            FeatureDesiredPropertiesDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(),
-                                    dittoHeaders, metadata);
-                    final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                            DeleteFeatureDesiredPropertiesResponse.of(thingId, featureId, dittoHeaders), thing);
-                    return ResultFactory.>newMutationResult(command, event, response);
+                    final CompletionStage validatedStage =
+                            buildValidatedStage(command, thing);
+                    final CompletionStage> eventStage =
+                            validatedStage.thenApply(deleteFeatureDesiredProperties ->
+                                    FeatureDesiredPropertiesDeleted.of(thingId, featureId, nextRevision,
+                                            getEventTimestamp(),
+                                            dittoHeaders, metadata)
+                            );
+                    final CompletionStage responseStage = validatedStage
+                            .thenApply(deleteFeatureDesiredProperties ->
+                                    appendETagHeaderIfProvided(deleteFeatureDesiredProperties,
+                                            DeleteFeatureDesiredPropertiesResponse.of(thingId, featureId, dittoHeaders),
+                                            thing)
+                            );
+
+                    return ResultFactory.newMutationResult(command, eventStage, responseStage);
                 })
                 .orElseGet(() -> ResultFactory.newErrorResult(
                         ExceptionFactory.featureDesiredPropertiesNotFound(thingId, featureId, dittoHeaders), command));
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategy.java
index 7c2511bbed..a99093bbdf 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategy.java
@@ -13,20 +13,22 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonPointer;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonPointer;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDesiredProperty;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDesiredPropertyResponse;
 import org.eclipse.ditto.things.model.signals.events.FeatureDesiredPropertyDeleted;
@@ -36,13 +38,16 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureDesiredProperty} command.
  */
 @Immutable
-final class DeleteFeatureDesiredPropertyStrategy extends AbstractThingCommandStrategy {
+final class DeleteFeatureDesiredPropertyStrategy extends
+        AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeatureDesiredPropertyStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteFeatureDesiredPropertyStrategy() {
-        super(DeleteFeatureDesiredProperty.class);
+    DeleteFeatureDesiredPropertyStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeatureDesiredProperty.class, actorSystem);
     }
 
     @Override
@@ -60,6 +65,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final DeleteFeatureDesiredProperty command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final DeleteFeatureDesiredProperty command, final @Nullable Thing thing) {
         return getEntityOrThrow(thing).getFeatures()
                 .flatMap(features -> features.getFeature(command.getFeatureId()));
@@ -79,14 +105,22 @@ private Result> getDeleteFeatureDesiredPropertyResult(final Featur
 
         return feature.getDesiredProperties()
                 .flatMap(desiredProperties -> desiredProperties.getValue(desiredPropertyPointer))
-                .>>map(featureDesiredProperty -> {
-                    final ThingEvent event =
-                            FeatureDesiredPropertyDeleted.of(thingId, featureId, desiredPropertyPointer, nextRevision,
-                                    getEventTimestamp(), dittoHeaders, metadata);
-                    final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                            DeleteFeatureDesiredPropertyResponse.of(thingId, featureId, desiredPropertyPointer,
-                                    dittoHeaders), thing);
-                    return ResultFactory.newMutationResult(command, event, response);
+                .map(featureDesiredProperty -> {
+                    final CompletionStage validatedStage =
+                            buildValidatedStage(command, thing);
+                    final CompletionStage> eventStage =
+                            validatedStage.thenApply(deleteFeatureDesiredProperty ->
+                                    FeatureDesiredPropertyDeleted.of(thingId, featureId, desiredPropertyPointer,
+                                            nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+                            );
+                    final CompletionStage responseStage = validatedStage
+                            .thenApply(deleteFeatureDesiredProperty ->
+                                    appendETagHeaderIfProvided(deleteFeatureDesiredProperty,
+                                            DeleteFeatureDesiredPropertyResponse.of(thingId, featureId,
+                                                    desiredPropertyPointer, dittoHeaders), thing)
+                            );
+
+                    return ResultFactory.newMutationResult(command, eventStage, responseStage);
                 })
                 .orElseGet(() -> ResultFactory.newErrorResult(
                         ExceptionFactory.featureDesiredPropertyNotFound(thingId, featureId, desiredPropertyPointer,
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategy.java
index 1e0f50739a..ad164883fa 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureProperties;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeaturePropertiesResponse;
 import org.eclipse.ditto.things.model.signals.events.FeaturePropertiesDeleted;
@@ -35,13 +37,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureProperties} command.
  */
 @Immutable
-final class DeleteFeaturePropertiesStrategy extends AbstractThingCommandStrategy {
+final class DeleteFeaturePropertiesStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeaturePropertiesStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteFeaturePropertiesStrategy() {
-        super(DeleteFeatureProperties.class);
+    DeleteFeaturePropertiesStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeatureProperties.class, actorSystem);
     }
 
     @Override
@@ -59,6 +63,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final DeleteFeatureProperties command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final DeleteFeatureProperties command,
             final @Nullable Thing thing) {
 
@@ -77,12 +102,19 @@ private Result> getDeleteFeaturePropertiesResult(final Feature fea
 
         return feature.getProperties()
                 .map(featureProperties -> {
-                    final ThingEvent event =
-                            FeaturePropertiesDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(),
-                                    dittoHeaders, metadata);
-                    final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                            DeleteFeaturePropertiesResponse.of(thingId, featureId, dittoHeaders), thing);
-                    return ResultFactory.>newMutationResult(command, event, response);
+                    final CompletionStage validatedStage = buildValidatedStage(command, thing);
+                    final CompletionStage> eventStage =
+                            validatedStage.thenApply(deleteFeatureProperties ->
+                                    FeaturePropertiesDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(),
+                                            dittoHeaders, metadata)
+                            );
+                    final CompletionStage responseStage = validatedStage
+                            .thenApply(deleteFeatureDesiredProperties ->
+                                    appendETagHeaderIfProvided(deleteFeatureDesiredProperties,
+                                            DeleteFeaturePropertiesResponse.of(thingId, featureId, dittoHeaders), thing)
+                            );
+
+                    return ResultFactory.newMutationResult(command, eventStage, responseStage);
                 })
                 .orElseGet(() -> ResultFactory.newErrorResult(
                         ExceptionFactory.featurePropertiesNotFound(thingId, featureId, dittoHeaders), command));
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategy.java
index a7c9053890..c6e4dd26ce 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategy.java
@@ -13,20 +13,22 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonPointer;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonPointer;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureProperty;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeaturePropertyResponse;
 import org.eclipse.ditto.things.model.signals.events.FeaturePropertyDeleted;
@@ -36,13 +38,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureProperty} command.
  */
 @Immutable
-final class DeleteFeaturePropertyStrategy extends AbstractThingCommandStrategy {
+final class DeleteFeaturePropertyStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeaturePropertyStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteFeaturePropertyStrategy() {
-        super(DeleteFeatureProperty.class);
+    DeleteFeaturePropertyStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeatureProperty.class, actorSystem);
     }
 
     @Override
@@ -60,6 +64,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final DeleteFeatureProperty command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final DeleteFeatureProperty command, final @Nullable Thing thing) {
         return getEntityOrThrow(thing).getFeatures()
                 .flatMap(features -> features.getFeature(command.getFeatureId()));
@@ -77,12 +102,21 @@ private Result> getDeleteFeaturePropertyResult(final Feature featu
         return feature.getProperties()
                 .flatMap(featureProperties -> featureProperties.getValue(propertyPointer))
                 .map(featureProperty -> {
-                    final ThingEvent event =
-                            FeaturePropertyDeleted.of(thingId, featureId, propertyPointer, nextRevision,
-                                    getEventTimestamp(), dittoHeaders, metadata);
-                    final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                            DeleteFeaturePropertyResponse.of(thingId, featureId, propertyPointer, dittoHeaders), thing);
-                    return ResultFactory.>newMutationResult(command, event, response);
+                    final CompletionStage validatedStage =
+                            buildValidatedStage(command, thing);
+                    final CompletionStage> eventStage =
+                            validatedStage.thenApply(deleteFeatureProperty ->
+                                    FeaturePropertyDeleted.of(thingId, featureId, propertyPointer, nextRevision,
+                                            getEventTimestamp(), dittoHeaders, metadata)
+                            );
+                    final CompletionStage responseStage = validatedStage
+                            .thenApply(deleteFeatureProperty ->
+                                    appendETagHeaderIfProvided(deleteFeatureProperty,
+                                            DeleteFeaturePropertyResponse.of(thingId, featureId, propertyPointer,
+                                                    dittoHeaders), thing)
+                            );
+
+                    return ResultFactory.newMutationResult(command, eventStage, responseStage);
                 })
                 .orElseGet(() -> ResultFactory.newErrorResult(
                         ExceptionFactory.featurePropertyNotFound(thingId, featureId, propertyPointer, dittoHeaders),
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategy.java
index bd37a16f6f..d47da4626b 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeature;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureResponse;
 import org.eclipse.ditto.things.model.signals.events.FeatureDeleted;
@@ -35,13 +37,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeature} command.
  */
 @Immutable
-final class DeleteFeatureStrategy extends AbstractThingCommandStrategy {
+final class DeleteFeatureStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeatureStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteFeatureStrategy() {
-        super(DeleteFeature.class);
+    DeleteFeatureStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeature.class, actorSystem);
     }
 
     @Override
@@ -60,6 +64,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final DeleteFeature command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final DeleteFeature command, @Nullable final Thing thing) {
         final String featureId = command.getFeatureId();
 
@@ -73,12 +98,18 @@ private Result> getDeleteFeatureResult(final Context cont
         final ThingId thingId = context.getState();
         final String featureId = command.getFeatureId();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
-        final ThingEvent event =
-                FeatureDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                DeleteFeatureResponse.of(thingId, featureId, dittoHeaders), thing);
 
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(deleteFeature ->
+                FeatureDeleted.of(thingId, featureId, nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage
+                .thenApply(deleteFeature ->
+                        appendETagHeaderIfProvided(deleteFeature,
+                                DeleteFeatureResponse.of(thingId, featureId, dittoHeaders), thing)
+                );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategy.java
index d8001771e4..9adfc4a3b6 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Features;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatures;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeaturesResponse;
 import org.eclipse.ditto.things.model.signals.events.FeaturesDeleted;
@@ -35,13 +37,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatures} command.
  */
 @Immutable
-final class DeleteFeaturesStrategy extends AbstractThingCommandStrategy {
+final class DeleteFeaturesStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteFeaturesStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteFeaturesStrategy() {
-        super(DeleteFeatures.class);
+    DeleteFeaturesStrategy(final ActorSystem actorSystem) {
+        super(DeleteFeatures.class, actorSystem);
     }
 
     @Override
@@ -54,30 +58,44 @@ protected Result> doApply(final Context context,
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
         return extractFeatures(thing)
-                .map(features ->
-                        ResultFactory.>newMutationResult(command,
-                                getEventToPersist(context, nextRevision, dittoHeaders, metadata),
-                                getResponse(context, command, thing))
+                .map(features -> {
+                            final CompletionStage validatedStage = buildValidatedStage(command, thing);
+                            final CompletionStage> eventStage = validatedStage.thenApply(deleteFeatures ->
+                                    FeaturesDeleted.of(context.getState(), nextRevision, getEventTimestamp(),
+                                            dittoHeaders, metadata)
+                            );
+                            final CompletionStage responseStage = validatedStage
+                                    .thenApply(deleteFeatures ->
+                                            appendETagHeaderIfProvided(deleteFeatures,
+                                                    DeleteFeaturesResponse.of(context.getState(), command.getDittoHeaders()),
+                                                    thing)
+                                    );
+
+                            return ResultFactory.newMutationResult(command, eventStage, responseStage);
+                        }
                 )
                 .orElseGet(() ->
                         ResultFactory.newErrorResult(ExceptionFactory.featuresNotFound(context.getState(),
                                 dittoHeaders), command));
     }
 
-    private Optional extractFeatures(final @Nullable Thing thing) {
-        return getEntityOrThrow(thing).getFeatures();
-    }
-
-    private static ThingEvent getEventToPersist(final Context context, final long nextRevision,
-            final DittoHeaders dittoHeaders, @Nullable final Metadata metadata) {
-
-        return FeaturesDeleted.of(context.getState(), nextRevision, getEventTimestamp(), dittoHeaders, metadata);
+    @Override
+    protected CompletionStage performWotValidation(
+            final DeleteFeatures command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingScopedDeletion(
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElse(null),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
     }
 
-    private WithDittoHeaders getResponse(final Context context, final DeleteFeatures command,
-            @Nullable final Thing thing) {
-        return appendETagHeaderIfProvided(command,
-                DeleteFeaturesResponse.of(context.getState(), command.getDittoHeaders()), thing);
+    private Optional extractFeatures(@Nullable final Thing thing) {
+        return getEntityOrThrow(thing).getFeatures();
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategy.java
index b4385adb8e..a8286397c6 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingDefinition;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingDefinitionNotAccessibleException;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteThingDefinition;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteThingDefinitionResponse;
@@ -36,14 +38,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.DeleteThingDefinition} command.
  */
 @Immutable
-final class DeleteThingDefinitionStrategy
-        extends AbstractThingCommandStrategy {
+final class DeleteThingDefinitionStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteThingDefinitionStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteThingDefinitionStrategy() {
-        super(DeleteThingDefinition.class);
+    DeleteThingDefinitionStrategy(final ActorSystem actorSystem) {
+        super(DeleteThingDefinition.class, actorSystem);
     }
 
     @Override
@@ -61,6 +64,18 @@ protected Result> doApply(final Context context,
                                 .build(), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final DeleteThingDefinition command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingDefinitionDeletion(Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getDefinition)
+                        .orElseThrow(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractDefinition(final @Nullable Thing thing) {
         return getEntityOrThrow(thing).getDefinition();
     }
@@ -71,12 +86,16 @@ private Result> getDeleteDefinitionResult(final Context c
         final ThingId thingId = context.getState();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                DeleteThingDefinitionResponse.of(thingId, dittoHeaders), thing);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(deleteFeature ->
+                ThingDefinitionDeleted.of(thingId, nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(deleteThingDefinition ->
+                appendETagHeaderIfProvided(deleteThingDefinition,
+                        DeleteThingDefinitionResponse.of(thingId, dittoHeaders), thing)
+        );
 
-        return ResultFactory.newMutationResult(command,
-                ThingDefinitionDeleted.of(thingId, nextRevision, getEventTimestamp(), dittoHeaders, metadata),
-                response);
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategy.java
index 9dcf9d2dd8..f333354459 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategy.java
@@ -13,18 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
-import org.eclipse.ditto.things.model.Thing;
-import org.eclipse.ditto.things.model.ThingId;
 import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
 import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.things.model.Thing;
+import org.eclipse.ditto.things.model.ThingId;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteThing;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteThingResponse;
 import org.eclipse.ditto.things.model.signals.events.ThingDeleted;
@@ -34,13 +37,15 @@
  * This strategy handles the {@link DeleteThing} command.
  */
 @Immutable
-final class DeleteThingStrategy extends AbstractThingCommandStrategy {
+final class DeleteThingStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code DeleteThingStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    DeleteThingStrategy() {
-        super(DeleteThing.class);
+    DeleteThingStrategy(final ActorSystem actorSystem) {
+        super(DeleteThing.class, actorSystem);
     }
 
     @Override
@@ -62,6 +67,14 @@ protected Result> doApply(final Context context,
         return ResultFactory.newMutationResult(command, event, response, false, true);
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final DeleteThing command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return CompletableFuture.completedFuture(command);
+    }
+
     @Override
     public Optional previousEntityTag(final DeleteThing command, @Nullable final Thing previousEntity) {
         return Optional.ofNullable(previousEntity).flatMap(EntityTag::fromEntity);
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategy.java
index 0447010174..51e211cee5 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategy.java
@@ -14,11 +14,13 @@
 
 import java.time.Instant;
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 import java.util.function.Supplier;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
@@ -46,16 +48,18 @@
  * This strategy handles the {@link MergeThing} command for an already existing Thing.
  */
 @Immutable
-final class MergeThingStrategy extends AbstractThingCommandStrategy {
+final class MergeThingStrategy extends AbstractThingModifyCommandStrategy {
 
     private static final ThingResourceMapper> ENTITY_TAG_MAPPER =
             ThingResourceMapper.from(EntityTagCalculator.getInstance());
 
     /**
      * Constructs a new {@code MergeThingStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    MergeThingStrategy() {
-        super(MergeThing.class);
+    MergeThingStrategy(final ActorSystem actorSystem) {
+        super(MergeThing.class, actorSystem);
     }
 
     @Override
@@ -70,9 +74,28 @@ protected Result> doApply(final Context context,
         return handleMergeExisting(context, nonNullThing, eventTs, nextRevision, command, metadata);
     }
 
-    private Result> handleMergeExisting(final Context context, final Thing thing,
-            final Instant eventTs, final long nextRevision, final MergeThing command,
-            @Nullable final Metadata metadata) {
+    @Override
+    protected CompletionStage performWotValidation(final MergeThing command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThing(
+                Optional.ofNullable(previewThing).flatMap(Thing::getDefinition)
+                        .or(() -> Optional.ofNullable(previousThing).flatMap(Thing::getDefinition))
+                        .orElse(null),
+                Optional.ofNullable(previewThing).orElseThrow(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
+    private Result> handleMergeExisting(final Context context,
+            final Thing thing,
+            final Instant eventTs,
+            final long nextRevision,
+            final MergeThing command,
+            @Nullable final Metadata metadata
+    ) {
         return handleMergeExistingV2WithV2Command(context, thing, eventTs, nextRevision, command, metadata);
     }
 
@@ -80,15 +103,21 @@ private Result> handleMergeExisting(final Context context
      * Handles a {@link MergeThing} command that was sent via API v2 and targets a Thing with API version V2.
      */
     private Result> handleMergeExistingV2WithV2Command(final Context context, final Thing thing,
-            final Instant eventTs, final long nextRevision, final MergeThing command,
-            @Nullable final Metadata metadata) {
+            final Instant eventTs,
+            final long nextRevision,
+            final MergeThing command,
+            @Nullable final Metadata metadata
+    ) {
         return applyMergeCommand(context, thing, eventTs, nextRevision, command, metadata);
     }
 
-    private Result> applyMergeCommand(final Context context, final Thing thing,
-            final Instant eventTs, final long nextRevision, final MergeThing command,
-            @Nullable final Metadata metadata) {
-
+    private Result> applyMergeCommand(final Context context,
+            final Thing thing,
+            final Instant eventTs,
+            final long nextRevision,
+            final MergeThing command,
+            @Nullable final Metadata metadata
+    ) {
         // make sure that the ThingMerged-Event contains all data contained in the resulting existingThing
         // (this is required e.g. for updating the search-index)
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
@@ -97,17 +126,25 @@ private Result> applyMergeCommand(final Context context,
 
         final Thing mergedThing = wrapException(() -> mergeThing(context, command, thing, eventTs, nextRevision),
                 command.getDittoHeaders());
-        final ThingEvent event =
-                ThingMerged.of(command.getEntityId(), path, value, nextRevision, eventTs, dittoHeaders, metadata);
-        final MergeThingResponse mergeThingResponse =
-                MergeThingResponse.of(command.getEntityId(), path, dittoHeaders);
 
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command, mergeThingResponse, mergedThing);
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing, mergedThing);
+
+        final CompletionStage> eventStage = validatedStage.thenApply(mergeThing ->
+                ThingMerged.of(mergeThing.getEntityId(), path, value, nextRevision, eventTs, dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(mergeThing ->
+                appendETagHeaderIfProvided(mergeThing, MergeThingResponse.of(command.getEntityId(), path, dittoHeaders),
+                        mergedThing)
+        );
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
-    private Thing mergeThing(final Context context, final MergeThing command, final Thing thing,
-            final Instant eventTs, final long nextRevision) {
+    private Thing mergeThing(final Context context,
+            final MergeThing command,
+            final Thing thing,
+            final Instant eventTs,
+            final long nextRevision
+    ) {
         final JsonObject existingThingJson = thing.toJson(FieldType.all());
         final JsonMergePatch jsonMergePatch = JsonMergePatch.of(command.getPath(),
                 command.getEntity().orElseGet(command::getValue));
@@ -140,9 +177,9 @@ private static  T wrapException(final Supplier supplier, final DittoHeader
         try {
             return supplier.get();
         } catch (final JsonRuntimeException
-                | IllegalArgumentException
-                | NullPointerException
-                | UnsupportedOperationException e) {
+                       | IllegalArgumentException
+                       | NullPointerException
+                       | UnsupportedOperationException e) {
             throw ThingMergeInvalidException.fromMessage(e.getMessage(), dittoHeaders);
         }
     }
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategy.java
index 4baca49012..a3c1b34cf4 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategy.java
@@ -13,21 +13,23 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
-import org.eclipse.ditto.json.JsonPointer;
-import org.eclipse.ditto.json.JsonValue;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
-import org.eclipse.ditto.things.model.Thing;
-import org.eclipse.ditto.things.model.ThingId;
 import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
 import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonPointer;
+import org.eclipse.ditto.json.JsonValue;
+import org.eclipse.ditto.things.model.Thing;
+import org.eclipse.ditto.things.model.ThingId;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandSizeValidator;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttribute;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttributeResponse;
@@ -39,13 +41,15 @@
  * This strategy handles the {@link ModifyAttribute} command.
  */
 @Immutable
-final class ModifyAttributeStrategy extends AbstractThingCommandStrategy {
+final class ModifyAttributeStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyAttributeStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyAttributeStrategy() {
-        super(ModifyAttribute.class);
+    ModifyAttributeStrategy(final ActorSystem actorSystem) {
+        super(ModifyAttribute.class, actorSystem);
     }
 
     @Override
@@ -57,7 +61,8 @@ protected Result> doApply(final Context context,
 
         final Thing nonNullThing = getEntityOrThrow(thing);
 
-        final JsonObject thingWithoutAttributeJsonObject = nonNullThing.removeAttribute(command.getAttributePointer()).toJson();
+        final JsonObject thingWithoutAttributeJsonObject =
+                nonNullThing.removeAttribute(command.getAttributePointer()).toJson();
         final JsonValue attributeJsonValue = command.getAttributeValue();
 
         ThingCommandSizeValidator.getInstance().ensureValidSize(
@@ -81,6 +86,20 @@ protected Result> doApply(final Context context,
                 .orElseGet(() -> getCreateResult(context, nextRevision, command, thing, metadata));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final ModifyAttribute command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingAttribute(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                command.getAttributePointer(),
+                command.getAttributeValue(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Result> getModifyResult(final Context context, final long nextRevision,
             final ModifyAttribute command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
 
@@ -88,13 +107,17 @@ private Result> getModifyResult(final Context context, fi
         final JsonPointer attributePointer = command.getAttributePointer();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                AttributeModified.of(thingId, attributePointer, command.getAttributeValue(), nextRevision,
-                        getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyAttributeResponse.modified(thingId, attributePointer, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyAttribute ->
+                AttributeModified.of(thingId, attributePointer, modifyAttribute.getAttributeValue(), nextRevision,
+                        getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyAttribute ->
+                appendETagHeaderIfProvided(modifyAttribute,
+                        ModifyAttributeResponse.modified(thingId, attributePointer, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
@@ -105,13 +128,17 @@ private Result> getCreateResult(final Context context, fi
         final JsonValue attributeValue = command.getAttributeValue();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyAttribute ->
                 AttributeCreated.of(thingId, attributePointer, attributeValue, nextRevision, getEventTimestamp(),
-                        dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyAttributeResponse.created(thingId, attributePointer, attributeValue, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyAttribute ->
+                appendETagHeaderIfProvided(modifyAttribute,
+                        ModifyAttributeResponse.created(thingId, attributePointer, attributeValue, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategy.java
index f9586b98f8..aadd20393f 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategy.java
@@ -13,20 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
-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.internal.utils.persistentactors.results.Result;
 import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.things.model.Thing;
+import org.eclipse.ditto.things.model.ThingId;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandSizeValidator;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttributes;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttributesResponse;
@@ -38,13 +39,15 @@
  * This strategy handles the {@link ModifyAttributes} command.
  */
 @Immutable
-final class ModifyAttributesStrategy extends AbstractThingCommandStrategy {
+final class ModifyAttributesStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyAttributesStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyAttributesStrategy() {
-        super(ModifyAttributes.class);
+    ModifyAttributesStrategy(final ActorSystem actorSystem) {
+        super(ModifyAttributes.class, actorSystem);
     }
 
     @Override
@@ -79,32 +82,58 @@ protected Result> doApply(final Context context,
                 .orElseGet(() -> getCreateResult(context, nextRevision, command, thing, metadata));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final ModifyAttributes command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingAttributes(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                command.getAttributes(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Result> getModifyResult(final Context context, final long nextRevision,
             final ModifyAttributes command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
         final ThingId thingId = context.getState();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                AttributesModified.of(thingId, command.getAttributes(), nextRevision, getEventTimestamp(),
-                        dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyAttributesResponse.modified(thingId, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyAttributes::getAttributes)
+                .thenApply(attributes ->
+                        AttributesModified.of(thingId, attributes, nextRevision, getEventTimestamp(), dittoHeaders,
+                                metadata)
+                );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyAttributes ->
+                appendETagHeaderIfProvided(modifyAttributes, ModifyAttributesResponse.modified(thingId, dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
             final ModifyAttributes command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
         final ThingId thingId = context.getState();
-        final Attributes attributes = command.getAttributes();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                AttributesCreated.of(thingId, attributes, nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyAttributesResponse.created(thingId, attributes, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyAttributes::getAttributes)
+                .thenApply(attributes ->
+                        AttributesCreated.of(thingId, attributes, nextRevision, getEventTimestamp(), dittoHeaders,
+                                metadata)
+                );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyAttributes ->
+                appendETagHeaderIfProvided(modifyAttributes,
+                        ModifyAttributesResponse.created(thingId, modifyAttributes.getAttributes(), dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategy.java
index 9129c946b4..f69f5fcefc 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDefinition;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDefinitionResponse;
 import org.eclipse.ditto.things.model.signals.events.FeatureDefinitionCreated;
@@ -36,13 +38,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDefinition} command.
  */
 @Immutable
-final class ModifyFeatureDefinitionStrategy extends AbstractThingCommandStrategy {
+final class ModifyFeatureDefinitionStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeatureDefinitionStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyFeatureDefinitionStrategy() {
-        super(ModifyFeatureDefinition.class);
+    ModifyFeatureDefinitionStrategy(final ActorSystem actorSystem) {
+        super(ModifyFeatureDefinition.class, actorSystem);
     }
 
     @Override
@@ -61,6 +65,22 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final ModifyFeatureDefinition command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureDefinitionModification(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                command.getDefinition(),
+                Optional.ofNullable(previousThing)
+                        .flatMap(t -> t.getFeatures().flatMap(f -> f.getFeature(command.getFeatureId())))
+                        .orElseThrow(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final ModifyFeatureDefinition command, final @Nullable Thing thing) {
         return getEntityOrThrow(thing).getFeatures()
                 .flatMap(features -> features.getFeature(command.getFeatureId()));
@@ -82,13 +102,17 @@ private Result> getModifyResult(final Context context, fi
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
         final String featureId = command.getFeatureId();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyFeatureDefinition ->
                 FeatureDefinitionModified.of(thingId, featureId, command.getDefinition(), nextRevision,
-                        getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureDefinitionResponse.modified(thingId, featureId, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureDefinition ->
+                appendETagHeaderIfProvided(modifyFeatureDefinition,
+                        ModifyFeatureDefinitionResponse.modified(thingId, featureId, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
@@ -98,13 +122,19 @@ private Result> getCreateResult(final Context context, fi
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
         final String featureId = command.getFeatureId();
 
-        final ThingEvent event = FeatureDefinitionCreated.of(thingId, featureId, command.getDefinition(),
-                nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureDefinitionResponse.created(thingId, featureId, command.getDefinition(), dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyFeatureDefinition ->
+                FeatureDefinitionCreated.of(thingId, featureId, command.getDefinition(),
+                        nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureDefinition ->
+                appendETagHeaderIfProvided(modifyFeatureDefinition,
+                        ModifyFeatureDefinitionResponse.created(thingId, featureId, command.getDefinition(),
+                                dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategy.java
index 7c5b4a0ad0..2b33294130 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategy.java
@@ -13,21 +13,22 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
 import org.eclipse.ditto.things.model.Feature;
-import org.eclipse.ditto.things.model.FeatureProperties;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandSizeValidator;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDesiredProperties;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDesiredPropertiesResponse;
@@ -40,13 +41,15 @@
  */
 @Immutable
 final class ModifyFeatureDesiredPropertiesStrategy
-        extends AbstractThingCommandStrategy {
+        extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeatureDesiredPropertiesStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyFeatureDesiredPropertiesStrategy() {
-        super(ModifyFeatureDesiredProperties.class);
+    ModifyFeatureDesiredPropertiesStrategy(final ActorSystem actorSystem) {
+        super(ModifyFeatureDesiredProperties.class, actorSystem);
     }
 
     @Override
@@ -68,13 +71,13 @@ protected Result> doApply(final Context context,
                 () -> {
                     final long lengthWithOutProperties = thingWithoutDesiredProperties.getUpperBoundForStringSize();
                     final long propertiesLength = propertiesJsonObject.getUpperBoundForStringSize()
-                            + "properties".length() + featureId.length() + 5L;
+                            + "desiredProperties".length() + featureId.length() + 5L;
                     return lengthWithOutProperties + propertiesLength;
                 },
                 () -> {
                     final long lengthWithOutProperties = thingWithoutDesiredProperties.toString().length();
                     final long propertiesLength = propertiesJsonObject.toString().length()
-                            + "properties".length() + featureId.length() + 5L;
+                            + "desiredProperties".length() + featureId.length() + 5L;
                     return lengthWithOutProperties + propertiesLength;
                 },
                 command::getDittoHeaders);
@@ -86,6 +89,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyFeatureDesiredProperties command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureProperties(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getDesiredProperties(),
+                true,
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final ModifyFeatureDesiredProperties command,
             @Nullable final Thing thing) {
 
@@ -116,13 +140,22 @@ private Result> getModifyResult(final Context context,
         final String featureId = command.getFeatureId();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                FeatureDesiredPropertiesModified.of(thingId, featureId, command.getDesiredProperties(), nextRevision,
-                        getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureDesiredPropertiesResponse.modified(context.getState(), featureId, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyFeatureDesiredProperties::getDesiredProperties)
+                .thenApply(desiredProperties ->
+                        FeatureDesiredPropertiesModified.of(thingId, featureId, desiredProperties, nextRevision,
+                                getEventTimestamp(), dittoHeaders, metadata)
+                );
+        final CompletionStage responseStage =
+                validatedStage.thenApply(modifyFeatureDesiredProperties ->
+                        appendETagHeaderIfProvided(modifyFeatureDesiredProperties,
+                                ModifyFeatureDesiredPropertiesResponse.modified(context.getState(), featureId,
+                                        dittoHeaders),
+                                thing)
+                );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context,
@@ -133,17 +166,25 @@ private Result> getCreateResult(final Context context,
 
         final ThingId thingId = context.getState();
         final String featureId = command.getFeatureId();
-        final FeatureProperties desiredProperties = command.getDesiredProperties();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                FeatureDesiredPropertiesCreated.of(thingId, featureId, desiredProperties, nextRevision,
-                        getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureDesiredPropertiesResponse.created(thingId, featureId, desiredProperties, dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyFeatureDesiredProperties::getDesiredProperties)
+                .thenApply(desiredProperties ->
+                        FeatureDesiredPropertiesCreated.of(thingId, featureId, desiredProperties, nextRevision,
+                                getEventTimestamp(), dittoHeaders, metadata)
+                );
+        final CompletionStage responseStage =
+                validatedStage.thenApply(modifyFeatureDesiredProperties ->
+                        appendETagHeaderIfProvided(modifyFeatureDesiredProperties,
+                                ModifyFeatureDesiredPropertiesResponse.created(thingId, featureId,
+                                        modifyFeatureDesiredProperties.getDesiredProperties(),
+                                        dittoHeaders),
+                                thing)
+                );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategy.java
index 91d5c02dd6..dc8296f472 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategy.java
@@ -13,22 +13,24 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
-import org.eclipse.ditto.json.JsonPointer;
-import org.eclipse.ditto.json.JsonValue;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonPointer;
+import org.eclipse.ditto.json.JsonValue;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandSizeValidator;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDesiredProperty;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDesiredPropertyResponse;
@@ -40,13 +42,16 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureDesiredProperty} command.
  */
 @Immutable
-final class ModifyFeatureDesiredPropertyStrategy extends AbstractThingCommandStrategy {
+final class ModifyFeatureDesiredPropertyStrategy
+        extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeatureDesiredPropertyStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyFeatureDesiredPropertyStrategy() {
-        super(ModifyFeatureDesiredProperty.class);
+    ModifyFeatureDesiredPropertyStrategy(final ActorSystem actorSystem) {
+        super(ModifyFeatureDesiredProperty.class, actorSystem);
     }
 
     @Override
@@ -87,6 +92,28 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyFeatureDesiredProperty command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureProperty(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getDesiredPropertyPointer(),
+                command.getDesiredPropertyValue(),
+                true,
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final ModifyFeatureDesiredProperty command, @Nullable final Thing thing) {
         return Optional.ofNullable(thing)
                 .flatMap(Thing::getFeatures)
@@ -116,15 +143,19 @@ private Result> getModifyResult(final Context context,
         final JsonPointer propertyPointer = command.getDesiredPropertyPointer();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyFeatureDesiredProperty ->
                 FeatureDesiredPropertyModified.of(command.getEntityId(), featureId, propertyPointer,
-                        command.getDesiredPropertyValue(), nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureDesiredPropertyResponse.modified(context.getState(), featureId, propertyPointer,
-                        dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        command.getDesiredPropertyValue(), nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureDesiredProperty ->
+                appendETagHeaderIfProvided(modifyFeatureDesiredProperty,
+                        ModifyFeatureDesiredPropertyResponse.modified(context.getState(), featureId, propertyPointer,
+                                dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context,
@@ -138,15 +169,19 @@ private Result> getCreateResult(final Context context,
         final JsonValue propertyValue = command.getDesiredPropertyValue();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyFeatureDesiredProperty ->
                 FeatureDesiredPropertyCreated.of(command.getEntityId(), featureId, propertyPointer, propertyValue,
-                        nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureDesiredPropertyResponse.created(context.getState(), featureId, propertyPointer,
-                        propertyValue, dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureDesiredProperty ->
+                appendETagHeaderIfProvided(modifyFeatureDesiredProperty,
+                        ModifyFeatureDesiredPropertyResponse.created(context.getState(), featureId, propertyPointer,
+                                propertyValue, dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategy.java
index 0590c18022..8f0c533e3a 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategy.java
@@ -13,21 +13,22 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
 import org.eclipse.ditto.things.model.Feature;
-import org.eclipse.ditto.things.model.FeatureProperties;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandSizeValidator;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureProperties;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeaturePropertiesResponse;
@@ -39,13 +40,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureProperties} command.
  */
 @Immutable
-final class ModifyFeaturePropertiesStrategy extends AbstractThingCommandStrategy {
+final class ModifyFeaturePropertiesStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeaturePropertiesStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyFeaturePropertiesStrategy() {
-        super(ModifyFeatureProperties.class);
+    ModifyFeaturePropertiesStrategy(final ActorSystem actorSystem) {
+        super(ModifyFeatureProperties.class, actorSystem);
     }
 
     @Override
@@ -84,6 +87,27 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyFeatureProperties command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureProperties(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getProperties(),
+                false,
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final ModifyFeatureProperties command, @Nullable final Thing thing) {
         return Optional.ofNullable(thing)
                 .flatMap(Thing::getFeatures)
@@ -106,13 +130,19 @@ private Result> getModifyResult(final Context context, fi
         final String featureId = command.getFeatureId();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                FeaturePropertiesModified.of(thingId, featureId, command.getProperties(), nextRevision,
-                        getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeaturePropertiesResponse.modified(context.getState(), featureId, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyFeatureProperties::getProperties)
+                .thenApply(properties ->
+                        FeaturePropertiesModified.of(thingId, featureId, properties, nextRevision,
+                                getEventTimestamp(), dittoHeaders, metadata)
+                );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureProperties ->
+                appendETagHeaderIfProvided(modifyFeatureProperties,
+                        ModifyFeaturePropertiesResponse.modified(context.getState(), featureId, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
@@ -120,18 +150,24 @@ private Result> getCreateResult(final Context context, fi
 
         final ThingId thingId = context.getState();
         final String featureId = command.getFeatureId();
-        final FeatureProperties featureProperties = command.getProperties();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event = FeaturePropertiesCreated.of(thingId, featureId, featureProperties, nextRevision,
-                getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeaturePropertiesResponse.created(thingId, featureId, featureProperties, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyFeatureProperties::getProperties)
+                .thenApply(properties ->
+                        FeaturePropertiesCreated.of(thingId, featureId, properties, nextRevision,
+                                getEventTimestamp(), dittoHeaders, metadata)
+                );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureProperties ->
+                appendETagHeaderIfProvided(modifyFeatureProperties,
+                        ModifyFeaturePropertiesResponse.created(thingId, featureId,
+                                modifyFeatureProperties.getProperties(), dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
-
     @Override
     public Optional previousEntityTag(final ModifyFeatureProperties command,
             @Nullable final Thing previousEntity) {
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategy.java
index dc57d21f1d..9bb251894b 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategy.java
@@ -13,22 +13,24 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
-import org.eclipse.ditto.json.JsonPointer;
-import org.eclipse.ditto.json.JsonValue;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonPointer;
+import org.eclipse.ditto.json.JsonValue;
 import org.eclipse.ditto.things.model.Feature;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.ThingCommandSizeValidator;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureProperty;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeaturePropertyResponse;
@@ -40,13 +42,15 @@
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatureProperty} command.
  */
 @Immutable
-final class ModifyFeaturePropertyStrategy extends AbstractThingCommandStrategy {
+final class ModifyFeaturePropertyStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeaturePropertyStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyFeaturePropertyStrategy() {
-        super(ModifyFeatureProperty.class);
+    ModifyFeaturePropertyStrategy(final ActorSystem actorSystem) {
+        super(ModifyFeatureProperty.class, actorSystem);
     }
 
     @Override
@@ -59,18 +63,22 @@ protected Result> doApply(final Context context,
         final String featureId = command.getFeatureId();
         final Thing nonNullThing = getEntityOrThrow(thing);
 
-        final JsonObject thingWithoutFeaturePropertyJsonObject = nonNullThing.removeFeatureProperty(featureId, command.getPropertyPointer()).toJson();
+        final JsonObject thingWithoutFeaturePropertyJsonObject =
+                nonNullThing.removeFeatureProperty(featureId, command.getPropertyPointer()).toJson();
         final JsonValue propertyValue = command.getPropertyValue();
 
         ThingCommandSizeValidator.getInstance().ensureValidSize(
                 () -> {
-                    final long lengthWithOutProperty = thingWithoutFeaturePropertyJsonObject.getUpperBoundForStringSize();
-                    final long propertyLength = propertyValue.getUpperBoundForStringSize() + command.getPropertyPointer().length() + 5L;
+                    final long lengthWithOutProperty =
+                            thingWithoutFeaturePropertyJsonObject.getUpperBoundForStringSize();
+                    final long propertyLength =
+                            propertyValue.getUpperBoundForStringSize() + command.getPropertyPointer().length() + 5L;
                     return lengthWithOutProperty + propertyLength;
                 },
                 () -> {
                     final long lengthWithOutProperty = thingWithoutFeaturePropertyJsonObject.toString().length();
-                    final long propertyLength = propertyValue.toString().length() + command.getPropertyPointer().length() + 5L;
+                    final long propertyLength =
+                            propertyValue.toString().length() + command.getPropertyPointer().length() + 5L;
                     return lengthWithOutProperty + propertyLength;
                 },
                 command::getDittoHeaders);
@@ -82,6 +90,28 @@ protected Result> doApply(final Context context,
                                 command.getDittoHeaders()), command));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyFeatureProperty command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatureProperty(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(f -> f.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeatureId(),
+                command.getPropertyPointer(),
+                command.getPropertyValue(),
+                false,
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final ModifyFeatureProperty command, @Nullable final Thing thing) {
         return Optional.ofNullable(thing)
                 .flatMap(Thing::getFeatures)
@@ -105,14 +135,19 @@ private Result> getModifyResult(final Context context, fi
         final JsonPointer propertyPointer = command.getPropertyPointer();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event = FeaturePropertyModified.of(command.getEntityId(), featureId, propertyPointer,
-                command.getPropertyValue(), nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeaturePropertyResponse.modified(context.getState(), featureId, propertyPointer,
-                        dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyFeatureProperty ->
+                FeaturePropertyModified.of(command.getEntityId(), featureId, propertyPointer,
+                        command.getPropertyValue(), nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureProperty ->
+                appendETagHeaderIfProvided(modifyFeatureProperty,
+                        ModifyFeaturePropertyResponse.modified(context.getState(), featureId, propertyPointer,
+                                dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
@@ -123,18 +158,21 @@ private Result> getCreateResult(final Context context, fi
         final JsonValue propertyValue = command.getPropertyValue();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyFeatureProperty ->
                 FeaturePropertyCreated.of(command.getEntityId(), featureId, propertyPointer, propertyValue,
-                        nextRevision, getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeaturePropertyResponse.created(context.getState(), featureId, propertyPointer,
-                        propertyValue, dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        nextRevision, getEventTimestamp(), dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyFeatureProperty ->
+                appendETagHeaderIfProvided(modifyFeatureProperty,
+                        ModifyFeaturePropertyResponse.created(context.getState(), featureId, propertyPointer,
+                                propertyValue, dittoHeaders),
+                        thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
-
     @Override
     public Optional previousEntityTag(final ModifyFeatureProperty command,
             @Nullable final Thing previousEntity) {
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategy.java
index c6b5ff6b8b..76b2616b69 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategy.java
@@ -22,6 +22,8 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
+import org.apache.pekko.japi.Pair;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
@@ -43,17 +45,12 @@
 import org.eclipse.ditto.things.model.signals.events.FeatureCreated;
 import org.eclipse.ditto.things.model.signals.events.FeatureModified;
 import org.eclipse.ditto.things.model.signals.events.ThingEvent;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
-
-import org.apache.pekko.actor.ActorSystem;
 
 /**
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeature} command.
  */
 @Immutable
-final class ModifyFeatureStrategy extends AbstractThingCommandStrategy {
-
-    private final WotThingDescriptionProvider wotThingDescriptionProvider;
+final class ModifyFeatureStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeatureStrategy} object.
@@ -61,8 +58,7 @@ final class ModifyFeatureStrategy extends AbstractThingCommandStrategy> doApply(final Context context,
                 command::getDittoHeaders);
 
         return extractFeature(command, nonNullThing)
-                .map(feature -> getModifyResult(context, nextRevision, command, thing, metadata))
+                .map(feature -> {
+                    final CompletionStage validatedStage = buildValidatedStage(command, thing);
+                    return getModifyResult(context, nextRevision, command, thing, metadata, validatedStage);
+                })
                 .orElseGet(() -> getCreateResult(context, nextRevision, command, thing, metadata));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyFeature command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeature(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                Optional.ofNullable(previousThing)
+                        .flatMap(Thing::getFeatures)
+                        .flatMap(features -> features.getFeature(command.getFeatureId()))
+                        .flatMap(Feature::getDefinition)
+                        .orElse(null),
+                command.getFeature(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Optional extractFeature(final ModifyFeature command, @Nullable final Thing thing) {
         return getEntityOrThrow(thing).getFeatures()
                 .flatMap(features -> features.getFeature(command.getFeatureId()));
     }
 
     private Result> getModifyResult(final Context context, final long nextRevision,
-            final ModifyFeature command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
+            final ModifyFeature command, @Nullable final Thing thing, @Nullable final Metadata metadata,
+            final CompletionStage validatedStage) {
 
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                FeatureModified.of(command.getEntityId(), command.getFeature(), nextRevision, getEventTimestamp(),
-                        dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeatureResponse.modified(context.getState(), command.getFeatureId(), dittoHeaders),
-                thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        final CompletionStage> eventStage =
+                validatedStage.thenApply(modifyFeature ->
+                        FeatureModified.of(command.getEntityId(), command.getFeature(), nextRevision,
+                                getEventTimestamp(),
+                                dittoHeaders, metadata));
+        final CompletionStage responseStage =
+                validatedStage.thenApply(modifyFeature ->
+                        appendETagHeaderIfProvided(modifyFeature,
+                                ModifyFeatureResponse.modified(context.getState(), command.getFeatureId(),
+                                        dittoHeaders),
+                                thing));
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
@@ -123,7 +147,7 @@ private Result> getCreateResult(final Context context, fi
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
         final Feature finalNewFeature = command.getFeature();
-        final CompletionStage featureStage = wotThingDescriptionProvider.provideFeatureSkeletonForCreation(
+        final CompletionStage featureStage = wotThingSkeletonGenerator.provideFeatureSkeletonForCreation(
                         finalNewFeature.getId(),
                         finalNewFeature.getDefinition().orElse(null),
                         dittoHeaders
@@ -155,14 +179,24 @@ private Result> getCreateResult(final Context context, fi
                         .orElse(finalNewFeature)
                 );
 
+        final CompletionStage> validatedStage =
+                featureStage.thenCompose(createdFeatureWithImplicits ->
+                        buildValidatedStage(
+                                ModifyFeature.of(command.getEntityId(), createdFeatureWithImplicits,
+                                        command.getDittoHeaders()),
+                                thing
+                        ).thenApply(modifyFeature -> new Pair<>(modifyFeature, createdFeatureWithImplicits))
+                );
         final CompletionStage> eventStage =
-                featureStage.thenApply(feature -> FeatureCreated.of(command.getEntityId(), feature, nextRevision,
-                        getEventTimestamp(), dittoHeaders,
-                        metadata));
+                validatedStage.thenApply(pair ->
+                        FeatureCreated.of(command.getEntityId(), pair.second(), nextRevision,
+                                getEventTimestamp(), dittoHeaders,
+                                metadata)
+                );
 
-        final CompletionStage response = featureStage.thenApply(modFeature ->
-                appendETagHeaderIfProvided(command,
-                        ModifyFeatureResponse.created(context.getState(), modFeature, dittoHeaders), thing)
+        final CompletionStage response = validatedStage.thenApply(pair ->
+                appendETagHeaderIfProvided(pair.first(),
+                        ModifyFeatureResponse.created(context.getState(), pair.second(), dittoHeaders), thing)
         );
 
         return ResultFactory.newMutationResult(command, eventStage, response);
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategy.java
index eb60ba4309..297ba5a91e 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategy.java
@@ -19,12 +19,14 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
@@ -47,17 +49,12 @@
 import org.eclipse.ditto.things.model.signals.events.FeaturesCreated;
 import org.eclipse.ditto.things.model.signals.events.FeaturesModified;
 import org.eclipse.ditto.things.model.signals.events.ThingEvent;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
-
-import org.apache.pekko.actor.ActorSystem;
 
 /**
  * This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeatures} command.
  */
 @Immutable
-final class ModifyFeaturesStrategy extends AbstractThingCommandStrategy {
-
-    private final WotThingDescriptionProvider wotThingDescriptionProvider;
+final class ModifyFeaturesStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyFeaturesStrategy} object.
@@ -65,8 +62,7 @@ final class ModifyFeaturesStrategy extends AbstractThingCommandStrategy> doApply(final Context context,
         ThingCommandSizeValidator.getInstance().ensureValidSize(
                 () -> {
                     final long lengthWithOutFeatures = thingWithoutFeaturesJsonObject.getUpperBoundForStringSize();
-                    final long featuresLength = featuresJsonObject.getUpperBoundForStringSize() + "features".length() + 5L;
+                    final long featuresLength =
+                            featuresJsonObject.getUpperBoundForStringSize() + "features".length() + 5L;
                     return lengthWithOutFeatures + featuresLength;
                 },
                 () -> {
@@ -99,18 +96,38 @@ protected Result> doApply(final Context context,
                 .orElseGet(() -> getCreateResult(context, nextRevision, command, thing, metadata));
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyFeatures command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateFeatures(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                command.getFeatures(),
+                command.getResourcePath(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Result> getModifyResult(final Context context, final long nextRevision,
             final ModifyFeatures command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
 
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
-                FeaturesModified.of(command.getEntityId(), command.getFeatures(), nextRevision,
-                        getEventTimestamp(), dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyFeaturesResponse.modified(context.getState(), dittoHeaders), thing);
+        final CompletionStage validationStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validationStage
+                .thenApply(ModifyFeatures::getFeatures)
+                .thenApply(features ->
+                        FeaturesModified.of(command.getEntityId(), features, nextRevision,
+                                getEventTimestamp(), dittoHeaders, metadata)
+                );
+        final CompletionStage responseStage = validationStage.thenApply(modifyFeatures ->
+                appendETagHeaderIfProvided(modifyFeatures,
+                        ModifyFeaturesResponse.modified(context.getState(), dittoHeaders), thing)
+        );
 
-        return ResultFactory.newMutationResult(command, event, response);
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
@@ -120,7 +137,7 @@ private Result> getCreateResult(final Context context, fi
 
         final List> featureStages = command.getFeatures()
                 .stream()
-                .map(feature -> wotThingDescriptionProvider.provideFeatureSkeletonForCreation(
+                .map(feature -> wotThingSkeletonGenerator.provideFeatureSkeletonForCreation(
                                         feature.getId(),
                                         feature.getDefinition().orElse(null),
                                         dittoHeaders
@@ -153,7 +170,7 @@ private Result> getCreateResult(final Context context, fi
                 .toList();
 
         final CompletableFuture featuresStage =
-                CompletableFuture.allOf(featureStages.toArray(new CompletableFuture[0]))
+                CompletableFuture.allOf(featureStages.toArray(CompletableFuture[]::new))
                         .thenApply(aVoid ->
                                 ThingsModelFactory.newFeatures(
                                         featureStages.stream()
@@ -164,15 +181,22 @@ private Result> getCreateResult(final Context context, fi
                                 )
                         );
 
-        final CompletableFuture> eventStage = featuresStage.thenApply(features ->
-                FeaturesCreated.of(command.getEntityId(), features, nextRevision, getEventTimestamp(),
-                        dittoHeaders, metadata
-                )
-        );
+        final Function> validationFunction = features ->
+                buildValidatedStage(command, thing);
 
+        final CompletableFuture> eventStage =
+                featuresStage.thenCompose(validationFunction)
+                        .thenApply(ModifyFeatures::getFeatures)
+                        .thenApply(features ->
+                                FeaturesCreated.of(command.getEntityId(), features, nextRevision, getEventTimestamp(),
+                                        dittoHeaders, metadata
+                                )
+                        );
         final CompletableFuture responseStage =
-                featuresStage.thenApply(features -> appendETagHeaderIfProvided(command,
-                        ModifyFeaturesResponse.created(context.getState(), features, dittoHeaders), thing)
+                featuresStage.thenCompose(validationFunction).thenApply(modifyFeatures ->
+                        appendETagHeaderIfProvided(modifyFeatures,
+                                ModifyFeaturesResponse.created(context.getState(), modifyFeatures.getFeatures(),
+                                        dittoHeaders), thing)
                 );
 
         return ResultFactory.newMutationResult(command, eventStage, responseStage);
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategy.java
index 619214bf16..9329ef1d8a 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategy.java
@@ -13,10 +13,13 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
@@ -35,13 +38,15 @@
  * This strategy handles the {@link ModifyPolicyId} command.
  */
 @Immutable
-final class ModifyPolicyIdStrategy extends AbstractThingCommandStrategy {
+final class ModifyPolicyIdStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyPolicyIdStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyPolicyIdStrategy() {
-        super(ModifyPolicyId.class);
+    ModifyPolicyIdStrategy(final ActorSystem actorSystem) {
+        super(ModifyPolicyId.class, actorSystem);
     }
 
     @Override
@@ -54,6 +59,14 @@ protected Result> doApply(final Context context,
         return getModifyResult(context, nextRevision, command, thing, metadata);
     }
 
+    @Override
+    protected CompletionStage performWotValidation(final ModifyPolicyId command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return CompletableFuture.completedFuture(command);
+    }
+
     private Optional extractPolicyId(final @Nullable Thing thing) {
         return getEntityOrThrow(thing).getPolicyId();
     }
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategy.java
index fd27eccf31..19f4bcdd1c 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategy.java
@@ -13,19 +13,21 @@
 package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;
 
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.Thing;
 import org.eclipse.ditto.things.model.ThingDefinition;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyThingDefinition;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyThingDefinitionResponse;
 import org.eclipse.ditto.things.model.signals.events.ThingDefinitionCreated;
@@ -36,13 +38,15 @@
  * This strategy handles the {@link ModifyThingDefinition} command.
  */
 @Immutable
-final class ModifyThingDefinitionStrategy extends AbstractThingCommandStrategy {
+final class ModifyThingDefinitionStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyThingDefinitionStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyThingDefinitionStrategy() {
-        super(ModifyThingDefinition.class);
+    ModifyThingDefinitionStrategy(final ActorSystem actorSystem) {
+        super(ModifyThingDefinition.class, actorSystem);
     }
 
     @Override
@@ -53,8 +57,21 @@ protected Result> doApply(final Context context,
             @Nullable final Metadata metadata) {
 
         return extractDefinition(thing)
-                .map(definition -> getModifyResult(context, nextRevision, command, thing, metadata))
-                .orElseGet(() -> getCreateResult(context, nextRevision, command, thing, metadata));
+                .map(definition -> getModifyResult(context, nextRevision, command, getEntityOrThrow(thing), metadata))
+                .orElseGet(() -> getCreateResult(context, nextRevision, command, getEntityOrThrow(thing), metadata));
+    }
+
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyThingDefinition command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThingDefinitionModification(
+                command.getDefinition(),
+                Optional.ofNullable(previousThing).orElseThrow(),
+                command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
     }
 
     private Optional extractDefinition(final @Nullable Thing thing) {
@@ -62,34 +79,42 @@ private Optional extractDefinition(final @Nullable Thing thing)
     }
 
     private Result> getModifyResult(final Context context, final long nextRevision,
-            final ModifyThingDefinition command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
+            final ModifyThingDefinition command, final Thing thing, @Nullable final Metadata metadata) {
 
         final ThingId thingId = context.getState();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyThingDefinition ->
                 ThingDefinitionModified.of(thingId, command.getDefinition(), nextRevision, getEventTimestamp(),
-                        dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyThingDefinitionResponse.modified(thingId, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        dittoHeaders, metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyThingDefinition ->
+                appendETagHeaderIfProvided(modifyThingDefinition,
+                        ModifyThingDefinitionResponse.modified(thingId, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     private Result> getCreateResult(final Context context, final long nextRevision,
-            final ModifyThingDefinition command, @Nullable final Thing thing, @Nullable final Metadata metadata) {
+            final ModifyThingDefinition command, final Thing thing, @Nullable final Metadata metadata) {
 
         final ThingId thingId = context.getState();
         final ThingDefinition definition = command.getDefinition();
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
-        final ThingEvent event =
+        final CompletionStage validatedStage = buildValidatedStage(command, thing);
+        final CompletionStage> eventStage = validatedStage.thenApply(modifyThingDefinition ->
                 ThingDefinitionCreated.of(thingId, definition, nextRevision, getEventTimestamp(), dittoHeaders,
-                        metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyThingDefinitionResponse.created(thingId, definition, dittoHeaders), thing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+                        metadata)
+        );
+        final CompletionStage responseStage = validatedStage.thenApply(modifyThingDefinition ->
+                appendETagHeaderIfProvided(modifyThingDefinition,
+                        ModifyThingDefinitionResponse.created(thingId, definition, dittoHeaders), thing)
+        );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategy.java
index 6e86082826..86b7e668de 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategy.java
@@ -16,10 +16,12 @@
 
 import java.time.Instant;
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
@@ -42,13 +44,15 @@
  * This strategy handles the {@link ModifyThing} command for an already existing Thing.
  */
 @Immutable
-final class ModifyThingStrategy extends AbstractThingCommandStrategy {
+final class ModifyThingStrategy extends AbstractThingModifyCommandStrategy {
 
     /**
      * Constructs a new {@code ModifyThingStrategy} object.
+     *
+     * @param actorSystem the actor system to use for loading the WoT extension.
      */
-    ModifyThingStrategy() {
-        super(ModifyThing.class);
+    ModifyThingStrategy(final ActorSystem actorSystem) {
+        super(ModifyThing.class, actorSystem);
     }
 
     @Override
@@ -71,6 +75,18 @@ protected Result> doApply(final Context context,
         return handleModifyExistingWithV2Command(context, nonNullThing, eventTs, nextRevision, command, metadata);
     }
 
+    @Override
+    protected CompletionStage performWotValidation(
+            final ModifyThing command,
+            @Nullable final Thing previousThing,
+            @Nullable final Thing previewThing
+    ) {
+        return wotThingModelValidator.validateThing(
+                Optional.ofNullable(previousThing).flatMap(Thing::getDefinition).orElse(null),
+                command.getThing(), command.getResourcePath(), command.getDittoHeaders()
+        ).thenApply(aVoid -> command);
+    }
+
     private Result> handleModifyExistingWithV2Command(final Context context, final Thing thing,
             final Instant eventTs, final long nextRevision, final ModifyThing command,
             @Nullable final Metadata metadata) {
@@ -94,13 +110,27 @@ private Result> applyModifyCommand(final Context context,
         final DittoHeaders dittoHeaders = command.getDittoHeaders();
 
         final Thing modifiedThing = applyThingModifications(command.getThing(), thing, eventTs, nextRevision);
-
-        final ThingEvent event =
-                ThingModified.of(modifiedThing, nextRevision, eventTs, dittoHeaders, metadata);
-        final WithDittoHeaders response = appendETagHeaderIfProvided(command,
-                ModifyThingResponse.modified(context.getState(), dittoHeaders), modifiedThing);
-
-        return ResultFactory.newMutationResult(command, event, response);
+        // validate based on potentially referenced Thing WoT TM/TD
+        final CompletionStage validatedStage = buildValidatedStage(
+                ModifyThing.of(command.getEntityId(),
+                        modifiedThing,
+                        command.getInitialPolicy().orElse(null),
+                        command.getDittoHeaders()
+                ), thing);
+
+        final CompletionStage> eventStage = validatedStage
+                .thenApply(ModifyThing::getThing)
+                .thenApply(theThing ->
+                        ThingModified.of(theThing, nextRevision, eventTs, dittoHeaders, metadata)
+                );
+        final CompletionStage responseStage = validatedStage
+                .thenApply(modifyThing ->
+                        appendETagHeaderIfProvided(modifyThing,
+                                ModifyThingResponse.modified(context.getState(), dittoHeaders),
+                                modifyThing.getThing())
+                );
+
+        return ResultFactory.newMutationResult(command, eventStage, responseStage);
     }
 
     /**
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategy.java
index 9a689a58a9..077099779d 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategy.java
@@ -17,16 +17,17 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import org.eclipse.ditto.json.JsonObject;
-import org.eclipse.ditto.json.JsonPointer;
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
+import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
+import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonPointer;
 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.internal.utils.persistentactors.results.Result;
-import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
 import org.eclipse.ditto.things.model.signals.commands.query.RetrieveAttribute;
 import org.eclipse.ditto.things.model.signals.commands.query.RetrieveAttributeResponse;
 import org.eclipse.ditto.things.model.signals.events.ThingEvent;
@@ -39,9 +40,11 @@ final class RetrieveAttributeStrategy extends AbstractThingCommandStrategy {
 
-    private final WotThingDescriptionProvider wotThingDescriptionProvider;
-
     /**
      * Constructs a new {@code RetrieveFeatureStrategy} object.
      *
      * @param actorSystem the actor system to use for loading the WoT extension.
      */
     RetrieveFeatureStrategy(final ActorSystem actorSystem) {
-        super(RetrieveFeature.class);
-        wotThingDescriptionProvider = WotThingDescriptionProvider.get(actorSystem);
+        super(RetrieveFeature.class, actorSystem);
     }
 
     @Override
@@ -96,7 +92,7 @@ private CompletionStage getRetrieveThingDescriptionResponse(@N
         if (thing != null) {
             return thing.getFeatures()
                     .flatMap(f -> f.getFeature(featureId))
-                    .map(feature -> wotThingDescriptionProvider
+                    .map(feature -> wotThingDescriptionGenerator
                             .provideFeatureTD(command.getEntityId(), thing, feature, command.getDittoHeaders())
                     )
                     .map(tdStage -> tdStage.thenApply(td ->
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategy.java
index b2b38c3d42..ecc943efe3 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategy.java
@@ -18,6 +18,7 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
@@ -43,9 +44,11 @@ final class RetrieveFeaturesStrategy extends AbstractThingCommandStrategy {
 
-    private final WotThingDescriptionProvider wotThingDescriptionProvider;
-
     /**
      * Constructs a new {@code RetrieveThingStrategy} object.
      *
      * @param actorSystem the actor system to use for loading the WoT extension.
      */
     RetrieveThingStrategy(final ActorSystem actorSystem) {
-        super(RetrieveThing.class);
-        wotThingDescriptionProvider = WotThingDescriptionProvider.get(actorSystem);
+        super(RetrieveThing.class, actorSystem);
     }
 
     @Override
@@ -126,7 +122,7 @@ private static JsonObject getThingJson(final Thing thing, final ThingQueryComman
     private CompletionStage getRetrieveThingDescriptionResponse(@Nullable final Thing thing,
             final RetrieveThing command) {
         if (thing != null) {
-            return wotThingDescriptionProvider
+            return wotThingDescriptionGenerator
                     .provideThingTD(thing.getDefinition().orElse(null),
                             command.getEntityId(),
                             thing,
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategy.java
index 0bbaa07d82..b29fcb3671 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategy.java
@@ -18,6 +18,7 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
 import org.eclipse.ditto.base.model.json.FieldType;
@@ -43,9 +44,11 @@ final class SudoRetrieveThingStrategy extends AbstractThingCommandStrategy> getCre
     }
 
     private void addThingStrategies(final ActorSystem system) {
-        addStrategy(new ThingConflictStrategy());
-        addStrategy(new ModifyThingStrategy());
+        addStrategy(new ThingConflictStrategy(system));
+        addStrategy(new ModifyThingStrategy(system));
         addStrategy(new RetrieveThingStrategy(system));
-        addStrategy(new DeleteThingStrategy());
-        addStrategy(new MergeThingStrategy());
+        addStrategy(new DeleteThingStrategy(system));
+        addStrategy(new MergeThingStrategy(system));
     }
 
-    private void addPolicyStrategies() {
-        addStrategy(new RetrievePolicyIdStrategy());
-        addStrategy(new ModifyPolicyIdStrategy());
+    private void addPolicyStrategies(final ActorSystem system) {
+        addStrategy(new RetrievePolicyIdStrategy(system));
+        addStrategy(new ModifyPolicyIdStrategy(system));
     }
 
-    private void addAttributesStrategies() {
-        addStrategy(new ModifyAttributesStrategy());
-        addStrategy(new ModifyAttributeStrategy());
-        addStrategy(new RetrieveAttributesStrategy());
-        addStrategy(new RetrieveAttributeStrategy());
-        addStrategy(new DeleteAttributesStrategy());
-        addStrategy(new DeleteAttributeStrategy());
+    private void addAttributesStrategies(final ActorSystem system) {
+        addStrategy(new ModifyAttributesStrategy(system));
+        addStrategy(new ModifyAttributeStrategy(system));
+        addStrategy(new RetrieveAttributesStrategy(system));
+        addStrategy(new RetrieveAttributeStrategy(system));
+        addStrategy(new DeleteAttributesStrategy(system));
+        addStrategy(new DeleteAttributeStrategy(system));
     }
 
-    private void addDefinitionStrategies() {
-        addStrategy(new ModifyThingDefinitionStrategy());
-        addStrategy(new RetrieveThingDefinitionStrategy());
-        addStrategy(new DeleteThingDefinitionStrategy());
+    private void addDefinitionStrategies(final ActorSystem system) {
+        addStrategy(new ModifyThingDefinitionStrategy(system));
+        addStrategy(new RetrieveThingDefinitionStrategy(system));
+        addStrategy(new DeleteThingDefinitionStrategy(system));
     }
 
     private void addFeaturesStrategies(final ActorSystem system) {
         addStrategy(new ModifyFeaturesStrategy(system));
         addStrategy(new ModifyFeatureStrategy(system));
-        addStrategy(new RetrieveFeaturesStrategy());
+        addStrategy(new RetrieveFeaturesStrategy(system));
         addStrategy(new RetrieveFeatureStrategy(system));
-        addStrategy(new DeleteFeaturesStrategy());
-        addStrategy(new DeleteFeatureStrategy());
+        addStrategy(new DeleteFeaturesStrategy(system));
+        addStrategy(new DeleteFeatureStrategy(system));
     }
 
-    private void addFeatureDefinitionStrategies() {
-        addStrategy(new ModifyFeatureDefinitionStrategy());
-        addStrategy(new RetrieveFeatureDefinitionStrategy());
-        addStrategy(new DeleteFeatureDefinitionStrategy());
+    private void addFeatureDefinitionStrategies(final ActorSystem system) {
+        addStrategy(new ModifyFeatureDefinitionStrategy(system));
+        addStrategy(new RetrieveFeatureDefinitionStrategy(system));
+        addStrategy(new DeleteFeatureDefinitionStrategy(system));
     }
 
-    private void addFeaturePropertiesStrategies() {
-        addStrategy(new ModifyFeaturePropertiesStrategy());
-        addStrategy(new ModifyFeaturePropertyStrategy());
-        addStrategy(new RetrieveFeaturePropertiesStrategy());
-        addStrategy(new RetrieveFeaturePropertyStrategy());
-        addStrategy(new DeleteFeaturePropertiesStrategy());
-        addStrategy(new DeleteFeaturePropertyStrategy());
+    private void addFeaturePropertiesStrategies(final ActorSystem system) {
+        addStrategy(new ModifyFeaturePropertiesStrategy(system));
+        addStrategy(new ModifyFeaturePropertyStrategy(system));
+        addStrategy(new RetrieveFeaturePropertiesStrategy(system));
+        addStrategy(new RetrieveFeaturePropertyStrategy(system));
+        addStrategy(new DeleteFeaturePropertiesStrategy(system));
+        addStrategy(new DeleteFeaturePropertyStrategy(system));
     }
 
-    private void addFeatureDesiredPropertiesStrategies() {
-        addStrategy(new ModifyFeatureDesiredPropertiesStrategy());
-        addStrategy(new ModifyFeatureDesiredPropertyStrategy());
-        addStrategy(new RetrieveFeatureDesiredPropertiesStrategy());
-        addStrategy(new RetrieveFeatureDesiredPropertyStrategy());
-        addStrategy(new DeleteFeatureDesiredPropertiesStrategy());
-        addStrategy(new DeleteFeatureDesiredPropertyStrategy());
+    private void addFeatureDesiredPropertiesStrategies(final ActorSystem system) {
+        addStrategy(new ModifyFeatureDesiredPropertiesStrategy(system));
+        addStrategy(new ModifyFeatureDesiredPropertyStrategy(system));
+        addStrategy(new RetrieveFeatureDesiredPropertiesStrategy(system));
+        addStrategy(new RetrieveFeatureDesiredPropertyStrategy(system));
+        addStrategy(new DeleteFeatureDesiredPropertiesStrategy(system));
+        addStrategy(new DeleteFeatureDesiredPropertyStrategy(system));
     }
 
-    private void addSudoStrategies() {
-        addStrategy(new SudoRetrieveThingStrategy());
+    private void addSudoStrategies(final ActorSystem system) {
+        addStrategy(new SudoRetrieveThingStrategy(system));
     }
 
     @Override
diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategy.java
index 2b1d2e40e6..1baee61899 100644
--- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategy.java
+++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategy.java
@@ -18,6 +18,7 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
 import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
@@ -36,9 +37,11 @@ final class ThingConflictStrategy extends AbstractThingCommandStrategy, T extends ThingModifiedEvent> T asser
         return assertModificationResult(result, expectedEventClass, expectedCommandResponse, becomeDeleted);
     }
 
+    protected static , T extends ThingModifiedEvent> T assertStagedModificationResult(
+            final CommandStrategy> underTest,
+            @Nullable final Thing thing,
+            final C command,
+            final Class expectedEventClass,
+            final CommandResponse expectedCommandResponse) {
+
+        final CommandStrategy.Context context = getDefaultContext();
+        final Result> result = applyStrategy(underTest, context, thing, command);
+
+        return assertStagedModificationResult(result, expectedEventClass, expectedCommandResponse, false);
+    }
+
     protected static , T extends ThingModifiedEvent> T assertStagedModificationResult(
             final CommandStrategy> underTest,
             @Nullable final Thing thing,
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategyTest.java
index c72feb488f..68113e197f 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributeStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -31,6 +32,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteAttributeStrategy}.
  */
@@ -40,7 +43,8 @@ public final class DeleteAttributeStrategyTest extends AbstractCommandStrategyTe
 
     @Before
     public void setUp() {
-        underTest = new DeleteAttributeStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteAttributeStrategy(system);
     }
 
     @Test
@@ -54,7 +58,7 @@ public void successfullyDeleteAttribute() {
         final CommandStrategy.Context context = getDefaultContext();
         final DeleteAttribute command = DeleteAttribute.of(context.getState(), attrPointer, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 AttributeDeleted.class,
                 DeleteAttributeResponse.of(context.getState(),
                         attrPointer, command.getDittoHeaders()));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategyTest.java
index 330c34064a..f2cd33900b 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteAttributesStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -26,6 +27,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteAttributesStrategy}.
  */
@@ -35,7 +38,8 @@ public final class DeleteAttributesStrategyTest extends AbstractCommandStrategyT
 
     @Before
     public void setUp() {
-        underTest = new DeleteAttributesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteAttributesStrategy(system);
     }
 
     @Test
@@ -48,7 +52,7 @@ public void successfullyDeleteAllAttributesFromThing() {
         final CommandStrategy.Context context = getDefaultContext();
         final DeleteAttributes command = DeleteAttributes.of(context.getState(), DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 AttributesDeleted.class,
                 DeleteAttributesResponse.of(context.getState(), command.getDittoHeaders()));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategyTest.java
index 7f0f2ec2f6..afe03a365c 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDefinitionStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -29,6 +30,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeatureDefinitionStrategy}.
  */
@@ -38,7 +41,8 @@ public final class DeleteFeatureDefinitionStrategyTest extends AbstractCommandSt
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeatureDefinitionStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeatureDefinitionStrategy(system);
     }
 
     @Test
@@ -53,7 +57,7 @@ public void successfullyDeleteFeatureDefinitionFromFeature() {
         final DeleteFeatureDefinition command =
                 DeleteFeatureDefinition.of(context.getState(), featureId, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureDefinitionDeleted.class,
                 DeleteFeatureDefinitionResponse.of(context.getState(), featureId, command.getDittoHeaders()));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategyTest.java
index e3d37d51e2..d8a3500ba8 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertiesStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -29,6 +30,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeatureDesiredPropertiesStrategy}.
  */
@@ -38,7 +41,8 @@ public final class DeleteFeatureDesiredPropertiesStrategyTest extends AbstractCo
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeatureDesiredPropertiesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeatureDesiredPropertiesStrategy(system);
     }
 
     @Test
@@ -53,7 +57,7 @@ public void successfullyDeleteFeatureDesiredPropertiesFromFeature() {
         final DeleteFeatureDesiredProperties command =
                 DeleteFeatureDesiredProperties.of(context.getState(), featureId, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureDesiredPropertiesDeleted.class,
                 DeleteFeatureDesiredPropertiesResponse.of(context.getState(), featureId, command.getDittoHeaders()));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategyTest.java
index 28b0b41132..03a96fe9f8 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureDesiredPropertyStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -32,6 +33,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeatureDesiredPropertyStrategy}.
  */
@@ -50,7 +53,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeatureDesiredPropertyStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeatureDesiredPropertyStrategy(system);
     }
 
     @Test
@@ -64,7 +68,7 @@ public void successfullyDeleteFeatureDesiredPropertyFromThing() {
         final DeleteFeatureDesiredProperty command =
                 DeleteFeatureDesiredProperty.of(context.getState(), featureId, propertyPointer, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureDesiredPropertyDeleted.class,
                 DeleteFeatureDesiredPropertyResponse.of(context.getState(),
                         command.getFeatureId(), propertyPointer, command.getDittoHeaders()));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategyTest.java
index 05558aa6d0..ddccc82637 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertiesStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -29,6 +30,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeaturePropertiesStrategy}.
  */
@@ -38,7 +41,8 @@ public final class DeleteFeaturePropertiesStrategyTest extends AbstractCommandSt
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeaturePropertiesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeaturePropertiesStrategy(system);
     }
 
     @Test
@@ -53,7 +57,7 @@ public void successfullyDeleteFeaturePropertiesFromFeature() {
         final DeleteFeatureProperties command =
                 DeleteFeatureProperties.of(context.getState(), featureId, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeaturePropertiesDeleted.class,
                 DeleteFeaturePropertiesResponse.of(context.getState(), featureId, command.getDittoHeaders()));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategyTest.java
index d2bf551920..d4395eefce 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturePropertyStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -32,6 +33,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeaturePropertyStrategy}.
  */
@@ -50,7 +53,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeaturePropertyStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeaturePropertyStrategy(system);
     }
 
     @Test
@@ -64,7 +68,7 @@ public void successfullyDeleteFeaturePropertyFromThing() {
         final DeleteFeatureProperty command =
                 DeleteFeatureProperty.of(context.getState(), featureId, propertyPointer, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeaturePropertyDeleted.class,
                 DeleteFeaturePropertyResponse.of(context.getState(),
                         command.getFeatureId(), propertyPointer, command.getDittoHeaders()));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategyTest.java
index 7f8057996f..ef9c4829d1 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeatureStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -24,11 +25,12 @@
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeature;
 import org.eclipse.ditto.things.model.signals.commands.modify.DeleteFeatureResponse;
 import org.eclipse.ditto.things.model.signals.events.FeatureDeleted;
-
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeatureStrategy}.
  */
@@ -45,7 +47,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeatureStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeatureStrategy(system);
     }
 
     @Test
@@ -59,7 +62,7 @@ public void successfullyDeleteFeatureFromThing() {
         final DeleteFeature command = DeleteFeature.of(context.getState(), featureId, DittoHeaders.newBuilder()
                 .build());
 
-        final FeatureDeleted event = assertModificationResult(underTest, THING_V2, command, FeatureDeleted.class,
+        assertStagedModificationResult(underTest, THING_V2, command, FeatureDeleted.class,
                 DeleteFeatureResponse.of(context.getState(), command.getFeatureId(), command.getDittoHeaders()));
     }
 
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategyTest.java
index bf97b625be..491309ad14 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteFeaturesStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -26,6 +27,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteFeaturesStrategy}.
  */
@@ -35,7 +38,8 @@ public final class DeleteFeaturesStrategyTest extends AbstractCommandStrategyTes
 
     @Before
     public void setUp() {
-        underTest = new DeleteFeaturesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteFeaturesStrategy(system);
     }
 
     @Test
@@ -48,7 +52,7 @@ public void successfullyDeleteFeaturesFromThing() {
         final CommandStrategy.Context context = getDefaultContext();
         final DeleteFeatures command = DeleteFeatures.of(context.getState(), DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeaturesDeleted.class,
                 DeleteFeaturesResponse.of(context.getState(), command.getDittoHeaders()));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategyTest.java
index e616ebbeaa..846aca44f9 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingDefinitionStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.ThingId;
@@ -26,6 +27,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteThingDefinitionStrategy}.
  */
@@ -35,7 +38,8 @@ public final class DeleteThingDefinitionStrategyTest extends AbstractCommandStra
 
     @Before
     public void setUp() {
-        underTest = new DeleteThingDefinitionStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteThingDefinitionStrategy(system);
     }
 
     @Test
@@ -48,7 +52,7 @@ public void successfullyDeleteDefinitionFromThing() {
         final CommandStrategy.Context context = getDefaultContext();
         final DeleteThingDefinition command = DeleteThingDefinition.of(context.getState(), DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 ThingDefinitionDeleted.class,
                 DeleteThingDefinitionResponse.of(context.getState(), command.getDittoHeaders()));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategyTest.java
index 9b615559a3..00a59859f7 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/DeleteThingStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.ThingId;
@@ -25,6 +26,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link DeleteThingStrategy}.
  */
@@ -34,7 +37,8 @@ public final class DeleteThingStrategyTest extends AbstractCommandStrategyTest {
 
     @Before
     public void setUp() {
-        underTest = new DeleteThingStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new DeleteThingStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategyTest.java
index d5c31beb63..8ee662f039 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/MergeThingStrategyTest.java
@@ -20,6 +20,7 @@
 
 import java.util.UUID;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.json.JsonObject;
@@ -36,6 +37,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link MergeThingStrategy}.
  */
@@ -45,7 +48,8 @@ public final class MergeThingStrategyTest extends AbstractCommandStrategyTest {
 
     @Before
     public void setUp() {
-        underTest = new MergeThingStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new MergeThingStrategy(system);
     }
 
     @Test
@@ -73,7 +77,7 @@ public void mergeThing() {
         final MergeThing mergeThing = MergeThing.of(thingId, path, thingJson, DittoHeaders.empty());
         final MergeThingResponse expectedCommandResponse =
                 ETagTestUtils.mergeThingResponse(existing, path, mergeThing.getDittoHeaders());
-        assertModificationResult(underTest, existing, mergeThing, ThingMerged.class, expectedCommandResponse);
+        assertStagedModificationResult(underTest, existing, mergeThing, ThingMerged.class, expectedCommandResponse);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategyTest.java
index b997b026e8..05ccdd4f68 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributeStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.json.JsonFactory;
@@ -30,6 +31,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyAttributeStrategy}.
  */
@@ -48,7 +51,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyAttributeStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyAttributeStrategy(system);
     }
 
     @Test
@@ -62,7 +66,7 @@ public void modifyAttributeOfThingWithoutAttributes() {
         final ModifyAttribute command =
                 ModifyAttribute.of(context.getState(), attributePointer, attributeValue, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.removeAttributes(), command,
+        assertStagedModificationResult(underTest, THING_V2.removeAttributes(), command,
                 AttributeCreated.class,
                 ETagTestUtils.modifyAttributeResponse(context.getState(), attributePointer, attributeValue,
                         command.getDittoHeaders(), true));
@@ -74,7 +78,7 @@ public void modifyAttributeOfThingWithoutThatAttribute() {
         final ModifyAttribute command =
                 ModifyAttribute.of(context.getState(), attributePointer, attributeValue, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 AttributeCreated.class,
                 ETagTestUtils.modifyAttributeResponse(context.getState(), attributePointer, attributeValue,
                         command.getDittoHeaders(), true));
@@ -90,7 +94,7 @@ public void modifyExistingAttribute() {
                 ModifyAttribute.of(context.getState(), existingAttributePointer, newAttributeValue,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 AttributeModified.class,
                 ETagTestUtils.modifyAttributeResponse(context.getState(), existingAttributePointer, newAttributeValue,
                         command.getDittoHeaders(), false));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategyTest.java
index 37327fde0f..8e09c9ec13 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyAttributesStrategyTest.java
@@ -16,11 +16,12 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
+import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.Attributes;
 import org.eclipse.ditto.things.model.TestConstants;
 import org.eclipse.ditto.things.model.ThingId;
-import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttributes;
 import org.eclipse.ditto.things.model.signals.events.AttributesCreated;
 import org.eclipse.ditto.things.model.signals.events.AttributesModified;
@@ -29,6 +30,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyAttributesStrategy}.
  */
@@ -47,7 +50,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyAttributesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyAttributesStrategy(system);
     }
 
     @Test
@@ -61,7 +65,7 @@ public void modifyAttributesOfThingWithoutAttributes() {
         final ModifyAttributes command =
                 ModifyAttributes.of(context.getState(), modifiedAttributes, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.removeAttributes(), command,
+        assertStagedModificationResult(underTest, THING_V2.removeAttributes(), command,
                 AttributesCreated.class,
                 ETagTestUtils.modifyAttributeResponse(context.getState(), modifiedAttributes, command.getDittoHeaders(), true));
     }
@@ -72,7 +76,7 @@ public void modifyAttributesOfThingWithAttributes() {
         final ModifyAttributes command =
                 ModifyAttributes.of(context.getState(), modifiedAttributes, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 AttributesModified.class,
                 ETagTestUtils.modifyAttributeResponse(context.getState(), modifiedAttributes, command.getDittoHeaders(), false));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategyTest.java
index a274450366..0204f69fb9 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -31,6 +32,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyFeatureDefinitionStrategy}.
  */
@@ -49,7 +52,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyFeatureDefinitionStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyFeatureDefinitionStrategy(system);
     }
 
     @Test
@@ -89,7 +93,7 @@ public void modifyFeatureDefinitionOfFeatureWithoutDefinition() {
                 ModifyFeatureDefinition.of(context.getState(), featureId, modifiedFeatureDefinition,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.setFeature(featureWithoutDefinition), command,
+        assertStagedModificationResult(underTest, THING_V2.setFeature(featureWithoutDefinition), command,
                 FeatureDefinitionCreated.class,
                 ETagTestUtils.modifyFeatureDefinitionResponse(context.getState(), featureId, command.getDefinition(),
                         command.getDittoHeaders(), true));
@@ -102,7 +106,7 @@ public void modifyExistingFeatureDefinition() {
                 ModifyFeatureDefinition.of(context.getState(), featureId, modifiedFeatureDefinition,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureDefinitionModified.class,
                 ETagTestUtils.modifyFeatureDefinitionResponse(context.getState(), featureId, modifiedFeatureDefinition,
                         command.getDittoHeaders(), false));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategyTest.java
index 8317665784..909ba7fb01 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertiesStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.common.DittoSystemProperties;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
@@ -37,6 +38,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyFeatureDesiredPropertiesStrategy}.
  */
@@ -58,7 +61,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyFeatureDesiredPropertiesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyFeatureDesiredPropertiesStrategy(system);
     }
 
     @Test
@@ -100,7 +104,7 @@ public void modifyFeatureDesiredPropertiesOfFeatureWithoutDesiredProperties() {
                 ModifyFeatureDesiredProperties.of(context.getState(), featureId, modifiedFeatureDesiredProperties,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.setFeature(featureWithoutProperties), command,
+        assertStagedModificationResult(underTest, THING_V2.setFeature(featureWithoutProperties), command,
                 FeatureDesiredPropertiesCreated.class,
                 ETagTestUtils.modifyFeatureDesiredPropertiesResponse(context.getState(), command.getFeatureId(),
                         command.getDesiredProperties(), command.getDittoHeaders(), true));
@@ -113,7 +117,7 @@ public void modifyExistingFeatureDesiredProperties() {
                 ModifyFeatureDesiredProperties.of(context.getState(), featureId, modifiedFeatureDesiredProperties,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureDesiredPropertiesModified.class,
                 ETagTestUtils.modifyFeatureDesiredPropertiesResponse(context.getState(), command.getFeatureId(),
                         modifiedFeatureDesiredProperties, command.getDittoHeaders(), false));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategyTest.java
index 4f7371f382..cf0c9fe9f7 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDesiredPropertyStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
@@ -32,11 +33,12 @@
 import org.eclipse.ditto.things.model.signals.events.FeatureDesiredPropertyCreated;
 import org.eclipse.ditto.things.model.signals.events.FeatureDesiredPropertyModified;
 import org.eclipse.ditto.things.service.persistence.actors.ETagTestUtils;
-
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyFeatureDesiredPropertyStrategy}.
  */
@@ -57,7 +59,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyFeatureDesiredPropertyStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyFeatureDesiredPropertyStrategy(system);
     }
 
     @Test
@@ -98,7 +101,7 @@ public void modifyFeatureDesiredPropertyOfFeatureWithoutProperties() {
                 ModifyFeatureDesiredProperty.of(context.getState(), featureId, propertyPointer, newPropertyValue,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.removeFeatureDesiredProperties(featureId), command,
+        assertStagedModificationResult(underTest, THING_V2.removeFeatureDesiredProperties(featureId), command,
                 FeatureDesiredPropertyCreated.class,
                 ETagTestUtils.modifyFeatureDesiredPropertyResponse(context.getState(), command.getFeatureId(),
                         command.getDesiredPropertyPointer(), command.getDesiredPropertyValue(), command.getDittoHeaders(), true));
@@ -111,7 +114,7 @@ public void modifyExistingFeatureDesiredProperty() {
                 ModifyFeatureDesiredProperty.of(context.getState(), featureId, propertyPointer, newPropertyValue,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureDesiredPropertyModified.class,
                 ETagTestUtils.modifyFeatureDesiredPropertyResponse(context.getState(), command.getFeatureId(),
                         command.getDesiredPropertyPointer(), command.getDesiredPropertyValue(), command.getDittoHeaders(), false));
@@ -129,7 +132,7 @@ public void setMetadata() {
                                 .build());
 
         final FeatureDesiredPropertyModified event =
-                assertModificationResult(underTest, THING_V2, command,
+                assertStagedModificationResult(underTest, THING_V2, command,
                         FeatureDesiredPropertyModified.class,
                         ETagTestUtils.modifyFeatureDesiredPropertyResponse(context.getState(), command.getFeatureId(),
                                 command.getDesiredPropertyPointer(), command.getDesiredPropertyValue(), command.getDittoHeaders(),
@@ -163,7 +166,7 @@ public void restrictMetadataUnderExistingFields() {
                                 .build());
 
         final FeatureDesiredPropertyModified event =
-                assertModificationResult(underTest, THING_V2, command,
+                assertStagedModificationResult(underTest, THING_V2, command,
                         FeatureDesiredPropertyModified.class,
                         ETagTestUtils.modifyFeatureDesiredPropertyResponse(context.getState(), command.getFeatureId(),
                                 command.getDesiredPropertyPointer(), command.getDesiredPropertyValue(), command.getDittoHeaders(),
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategyTest.java
index aa39f30fad..0521023855 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertiesStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.common.DittoSystemProperties;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
@@ -37,6 +38,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyFeaturePropertiesStrategy}.
  */
@@ -58,7 +61,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyFeaturePropertiesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyFeaturePropertiesStrategy(system);
     }
 
     @Test
@@ -100,7 +104,7 @@ public void modifyFeaturePropertiesOfFeatureWithoutProperties() {
                 ModifyFeatureProperties.of(context.getState(), featureId, modifiedFeatureProperties,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.setFeature(featureWithoutProperties), command,
+        assertStagedModificationResult(underTest, THING_V2.setFeature(featureWithoutProperties), command,
                 FeaturePropertiesCreated.class,
                 ETagTestUtils.modifyFeaturePropertiesResponse(context.getState(), command.getFeatureId(),
                         command.getProperties(), command.getDittoHeaders(), true));
@@ -113,7 +117,7 @@ public void modifyExistingFeatureProperties() {
                 ModifyFeatureProperties.of(context.getState(), featureId, modifiedFeatureProperties,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeaturePropertiesModified.class,
                 ETagTestUtils.modifyFeaturePropertiesResponse(context.getState(), command.getFeatureId(),
                         modifiedFeatureProperties, command.getDittoHeaders(), false));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategyTest.java
index 0c0447e72f..49ebfd3533 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturePropertyStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.entity.metadata.Metadata;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
@@ -32,11 +33,12 @@
 import org.eclipse.ditto.things.model.signals.events.FeaturePropertyCreated;
 import org.eclipse.ditto.things.model.signals.events.FeaturePropertyModified;
 import org.eclipse.ditto.things.service.persistence.actors.ETagTestUtils;
-
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyFeaturePropertyStrategy}.
  */
@@ -57,7 +59,8 @@ public static void initTestFixture() {
 
     @Before
     public void setUp() {
-        underTest = new ModifyFeaturePropertyStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyFeaturePropertyStrategy(system);
     }
 
     @Test
@@ -98,7 +101,7 @@ public void modifyFeaturePropertyOfFeatureWithoutProperties() {
                 ModifyFeatureProperty.of(context.getState(), featureId, propertyPointer, newPropertyValue,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.removeFeatureProperties(featureId), command,
+        assertStagedModificationResult(underTest, THING_V2.removeFeatureProperties(featureId), command,
                 FeaturePropertyCreated.class,
                 ETagTestUtils.modifyFeaturePropertyResponse(context.getState(), command.getFeatureId(),
                         command.getPropertyPointer(), command.getPropertyValue(), command.getDittoHeaders(), true));
@@ -111,7 +114,7 @@ public void modifyExistingFeatureProperty() {
                 ModifyFeatureProperty.of(context.getState(), featureId, propertyPointer, newPropertyValue,
                         DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeaturePropertyModified.class,
                 ETagTestUtils.modifyFeaturePropertyResponse(context.getState(), command.getFeatureId(),
                         command.getPropertyPointer(), command.getPropertyValue(), command.getDittoHeaders(), false));
@@ -129,7 +132,7 @@ public void setMetadata() {
                                 .build());
 
         final FeaturePropertyModified event =
-                assertModificationResult(underTest, THING_V2, command,
+                assertStagedModificationResult(underTest, THING_V2, command,
                         FeaturePropertyModified.class,
                         ETagTestUtils.modifyFeaturePropertyResponse(context.getState(), command.getFeatureId(),
                                 command.getPropertyPointer(), command.getPropertyValue(), command.getDittoHeaders(),
@@ -163,7 +166,7 @@ public void restrictMetadataUnderExistingFields() {
                                 .build());
 
         final FeaturePropertyModified event =
-                assertModificationResult(underTest, THING_V2, command,
+                assertStagedModificationResult(underTest, THING_V2, command,
                         FeaturePropertyModified.class,
                         ETagTestUtils.modifyFeaturePropertyResponse(context.getState(), command.getFeatureId(),
                                 command.getPropertyPointer(), command.getPropertyValue(), command.getDittoHeaders(),
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategyTest.java
index cf6f35cb9f..fa1899a1e4 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.Feature;
@@ -26,15 +27,13 @@
 import org.eclipse.ditto.things.model.signals.events.FeatureCreated;
 import org.eclipse.ditto.things.model.signals.events.FeatureModified;
 import org.eclipse.ditto.things.service.persistence.actors.ETagTestUtils;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
+import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import com.typesafe.config.ConfigFactory;
 
-import org.apache.pekko.actor.ActorSystem;
-
 /**
  * Unit test for {@link ModifyFeatureStrategy}.
  */
@@ -58,7 +57,7 @@ public void setUp() {
     @Test
     public void assertImmutability() {
         assertInstancesOf(ModifyFeatureStrategy.class, areImmutable(),
-                provided(WotThingDescriptionProvider.class).areAlsoImmutable());
+                provided(WotThingDescriptionGenerator.class).areAlsoImmutable());
     }
 
     @Test
@@ -86,7 +85,7 @@ public void modifyExistingFeature() {
         final CommandStrategy.Context context = getDefaultContext();
         final ModifyFeature command = ModifyFeature.of(context.getState(), modifiedFeature, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeatureModified.class,
                 ETagTestUtils.modifyFeatureResponse(context.getState(), command.getFeature(), command.getDittoHeaders(), false));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategyTest.java
index 9e04f3b4b7..143a819def 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeaturesStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.json.JsonObject;
@@ -33,15 +34,13 @@
 import org.eclipse.ditto.things.model.signals.events.FeaturesCreated;
 import org.eclipse.ditto.things.model.signals.events.FeaturesModified;
 import org.eclipse.ditto.things.service.persistence.actors.ETagTestUtils;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
+import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import com.typesafe.config.ConfigFactory;
 
-import org.apache.pekko.actor.ActorSystem;
-
 /**
  * Unit test for {@link ModifyFeaturesStrategy}.
  */
@@ -69,7 +68,7 @@ public void setUp() {
     @Test
     public void assertImmutability() {
         assertInstancesOf(ModifyFeaturesStrategy.class, areImmutable(),
-                provided(WotThingDescriptionProvider.class).areAlsoImmutable());
+                provided(WotThingDescriptionGenerator.class).areAlsoImmutable());
     }
 
     @Test
@@ -87,7 +86,7 @@ public void modifyExistingFeatures() {
         final CommandStrategy.Context context = getDefaultContext();
         final ModifyFeatures command = ModifyFeatures.of(context.getState(), modifiedFeatures, DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 FeaturesModified.class,
                 ETagTestUtils.modifyFeaturesResponse(context.getState(), modifiedFeatures, command.getDittoHeaders(), false));
     }
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategyTest.java
index 96e22b7afc..6c07417352 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyPolicyIdStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.ThingId;
@@ -26,6 +27,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyPolicyIdStrategy}.
  */
@@ -35,7 +38,8 @@ public final class ModifyPolicyIdStrategyTest extends AbstractCommandStrategyTes
 
     @Before
     public void setUp() {
-        underTest = new ModifyPolicyIdStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyPolicyIdStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategyTest.java
index e8eb14f630..d0f00e8bb0 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingDefinitionStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.ThingId;
@@ -27,6 +28,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyThingDefinitionStrategy}.
  */
@@ -36,7 +39,8 @@ public final class ModifyThingDefinitionStrategyTest extends AbstractCommandStra
 
     @Before
     public void setUp() {
-        underTest = new ModifyThingDefinitionStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyThingDefinitionStrategy(system);
     }
 
     @Test
@@ -50,7 +54,7 @@ public void modifyDefinitionOnThingWithoutDefinition() {
         final ModifyThingDefinition command = ModifyThingDefinition.of(context.getState(), DEFINITION,
                 DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2.toBuilder().setDefinition(null).build(), command,
+        assertStagedModificationResult(underTest, THING_V2.toBuilder().setDefinition(null).build(), command,
                 ThingDefinitionCreated.class, ETagTestUtils.modifyThingDefinitionResponse(context.getState(), command.getDefinition(),
                         command.getDittoHeaders(), true));
     }
@@ -61,7 +65,7 @@ public void modifyExistingDefinition() {
         final ModifyThingDefinition command = ModifyThingDefinition.of(context.getState(), DEFINITION,
                 DittoHeaders.empty());
 
-        assertModificationResult(underTest, THING_V2, command,
+        assertStagedModificationResult(underTest, THING_V2, command,
                 ThingDefinitionModified.class, ETagTestUtils.modifyThingDefinitionResponse(context.getState(),
                         command.getDefinition(),
                         command.getDittoHeaders(), false));
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategyTest.java
index 2f050c3d87..452d949171 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyThingStrategyTest.java
@@ -18,6 +18,7 @@
 
 import java.time.Instant;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.Thing;
@@ -29,6 +30,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ModifyThingStrategy}.
  */
@@ -39,7 +42,8 @@ public final class ModifyThingStrategyTest extends AbstractCommandStrategyTest {
 
     @Before
     public void setUp() {
-        underTest = new ModifyThingStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new ModifyThingStrategy(system);
     }
 
     @Test
@@ -64,7 +68,7 @@ public void modifyExisting() {
 
         final ModifyThing modifyThing = ModifyThing.of(thingId, thing, null, DittoHeaders.empty());
 
-        assertModificationResult(underTest, existing, modifyThing,
+        assertStagedModificationResult(underTest, existing, modifyThing,
                 ThingModified.class, ETagTestUtils.modifyThingResponse(existing, thing, modifyThing.getDittoHeaders(), false));
     }
 
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategyTest.java
index ec4013a10d..6e68cda0ae 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributeStrategyTest.java
@@ -16,6 +16,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -28,6 +29,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveAttributeStrategy}.
  */
@@ -37,7 +40,8 @@ public final class RetrieveAttributeStrategyTest extends AbstractCommandStrategy
 
     @Before
     public void setUp() {
-        underTest = new RetrieveAttributeStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveAttributeStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributesStrategyTest.java
index fdd5b3fde0..5a5a3bff02 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveAttributesStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -29,6 +30,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveAttributesStrategy}.
  */
@@ -38,7 +41,8 @@ public final class RetrieveAttributesStrategyTest extends AbstractCommandStrateg
 
     @Before
     public void setUp() {
-        underTest = new RetrieveAttributesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveAttributesStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDefinitionStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDefinitionStrategyTest.java
index 0c14629058..777c780770 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDefinitionStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDefinitionStrategyTest.java
@@ -19,6 +19,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -29,6 +30,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveFeatureDefinitionStrategy}.
  */
@@ -38,7 +41,8 @@ public final class RetrieveFeatureDefinitionStrategyTest extends AbstractCommand
 
     @Before
     public void setUp() {
-        underTest = new RetrieveFeatureDefinitionStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveFeatureDefinitionStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertiesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertiesStrategyTest.java
index 91decb307f..836f1f3305 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertiesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertiesStrategyTest.java
@@ -19,6 +19,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -32,6 +33,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveFeatureDesiredPropertiesStrategy}.
  */
@@ -41,7 +44,8 @@ public final class RetrieveFeatureDesiredPropertiesStrategyTest extends Abstract
 
     @Before
     public void setUp() {
-        underTest = new RetrieveFeatureDesiredPropertiesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveFeatureDesiredPropertiesStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertyStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertyStrategyTest.java
index 02a84abc0d..cad6dc5763 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertyStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureDesiredPropertyStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -30,6 +31,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveFeatureDesiredPropertyStrategy}.
  */
@@ -39,7 +42,8 @@ public final class RetrieveFeatureDesiredPropertyStrategyTest extends AbstractCo
 
     @Before
     public void setUp() {
-        underTest = new RetrieveFeatureDesiredPropertyStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveFeatureDesiredPropertyStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertiesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertiesStrategyTest.java
index 71470f2fed..af4b45b0ea 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertiesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertiesStrategyTest.java
@@ -19,6 +19,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -32,6 +33,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveFeaturePropertiesStrategy}.
  */
@@ -41,7 +44,8 @@ public final class RetrieveFeaturePropertiesStrategyTest extends AbstractCommand
 
     @Before
     public void setUp() {
-        underTest = new RetrieveFeaturePropertiesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveFeaturePropertiesStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertyStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertyStrategyTest.java
index 8e6f26e815..9930ce8188 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertyStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturePropertyStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -30,6 +31,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveFeaturePropertyStrategy}.
  */
@@ -39,7 +42,8 @@ public final class RetrieveFeaturePropertyStrategyTest extends AbstractCommandSt
 
     @Before
     public void setUp() {
-        underTest = new RetrieveFeaturePropertyStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveFeaturePropertyStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureStrategyTest.java
index 8ce41cb94b..7f2661bdff 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeatureStrategyTest.java
@@ -19,6 +19,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -26,14 +27,12 @@
 import org.eclipse.ditto.things.model.signals.commands.query.RetrieveFeature;
 import org.eclipse.ditto.things.model.signals.commands.query.RetrieveFeatureResponse;
 import org.eclipse.ditto.things.service.persistence.actors.ETagTestUtils;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
+import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator;
 import org.junit.Before;
 import org.junit.Test;
 
 import com.typesafe.config.ConfigFactory;
 
-import org.apache.pekko.actor.ActorSystem;
-
 /**
  * Unit test for {@link RetrieveFeatureStrategy}.
  */
@@ -50,7 +49,7 @@ public void setUp() {
     @Test
     public void assertImmutability() {
         assertInstancesOf(RetrieveFeatureStrategy.class, areImmutable(),
-                provided(WotThingDescriptionProvider.class).areAlsoImmutable());
+                provided(WotThingDescriptionGenerator.class).areAlsoImmutable());
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategyTest.java
index 4091d57d4d..968341f33f 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveFeaturesStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -34,6 +35,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveFeaturesStrategy}.
  */
@@ -43,7 +46,8 @@ public final class RetrieveFeaturesStrategyTest extends AbstractCommandStrategyT
 
     @Before
     public void setUp() {
-        underTest = new RetrieveFeaturesStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveFeaturesStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrievePolicyIdStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrievePolicyIdStrategyTest.java
index fcca0b43aa..404cc99dba 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrievePolicyIdStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrievePolicyIdStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.ThingId;
@@ -27,6 +28,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrievePolicyIdStrategy}.
  */
@@ -36,7 +39,8 @@ public final class RetrievePolicyIdStrategyTest extends AbstractCommandStrategyT
 
     @Before
     public void setUp() {
-        underTest = new RetrievePolicyIdStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrievePolicyIdStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingDefinitionStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingDefinitionStrategyTest.java
index 663096f71c..6b7482bbeb 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingDefinitionStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingDefinitionStrategyTest.java
@@ -17,6 +17,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
 import org.eclipse.ditto.things.model.ThingId;
@@ -27,6 +28,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link RetrieveThingDefinitionStrategy}.
  */
@@ -36,7 +39,8 @@ public final class RetrieveThingDefinitionStrategyTest extends AbstractCommandSt
 
     @Before
     public void setUp() {
-        underTest = new RetrieveThingDefinitionStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new RetrieveThingDefinitionStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingStrategyTest.java
index 81d1cf8d36..8f517fc756 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/RetrieveThingStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
 import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
@@ -32,14 +33,12 @@
 import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing;
 import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse;
 import org.eclipse.ditto.things.service.persistence.actors.ETagTestUtils;
-import org.eclipse.ditto.wot.integration.provider.WotThingDescriptionProvider;
+import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator;
 import org.junit.Before;
 import org.junit.Test;
 
 import com.typesafe.config.ConfigFactory;
 
-import org.apache.pekko.actor.ActorSystem;
-
 /**
  * Unit test for {@link RetrieveThingStrategy}.
  */
@@ -56,7 +55,7 @@ public void setUp() {
     @Test
     public void assertImmutability() {
         assertInstancesOf(RetrieveThingStrategy.class, areImmutable(),
-                provided(WotThingDescriptionProvider.class).areAlsoImmutable());
+                provided(WotThingDescriptionGenerator.class).areAlsoImmutable());
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategyTest.java
index 6c5105f6fb..59966e17bf 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/SudoRetrieveThingStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
 import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.json.FieldType;
 import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
@@ -36,6 +37,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link SudoRetrieveThingStrategy}.
  */
@@ -45,7 +48,8 @@ public final class SudoRetrieveThingStrategyTest extends AbstractCommandStrategy
 
     @Before
     public void setUp() {
-        underTest = new SudoRetrieveThingStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        underTest = new SudoRetrieveThingStrategy(system);
     }
 
     @Test
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java
index 454548ba81..33513afca9 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java
@@ -20,6 +20,7 @@
 
 import java.util.concurrent.CompletionStage;
 
+import org.apache.pekko.actor.ActorSystem;
 import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
 import org.eclipse.ditto.base.model.headers.DittoHeaders;
 import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
@@ -42,6 +43,8 @@
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import com.typesafe.config.ConfigFactory;
+
 /**
  * Unit test for {@link ThingConflictStrategy}.
  */
@@ -58,7 +61,8 @@ public void assertImmutability() {
 
     @Test
     public void createConflictResultWithoutPrecondition() {
-        final ThingConflictStrategy underTest = new ThingConflictStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        final ThingConflictStrategy underTest = new ThingConflictStrategy(system);
         final ThingId thingId = ThingId.of("thing:id");
         final Thing thing = ThingsModelFactory.newThingBuilder().setId(thingId).setRevision(25L).build();
         final CommandStrategy.Context context = DefaultContext.getInstance(thingId,
@@ -70,7 +74,8 @@ public void createConflictResultWithoutPrecondition() {
 
     @Test
     public void createPreconditionFailedResultWithPrecondition() {
-        final ThingConflictStrategy underTest = new ThingConflictStrategy();
+        final ActorSystem system = ActorSystem.create("test", ConfigFactory.load("test"));
+        final ThingConflictStrategy underTest = new ThingConflictStrategy(system);
         final ThingId thingId = ThingId.of("thing:id");
         final Thing thing = ThingsModelFactory.newThingBuilder().setId(thingId).setRevision(25L).build();
         final CommandStrategy.Context context = DefaultContext.getInstance(thingId,
diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/starter/ThingsServiceGlobalErrorRegistryTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/starter/ThingsServiceGlobalErrorRegistryTest.java
index 2923fe2644..21a8a341c7 100644
--- a/things/service/src/test/java/org/eclipse/ditto/things/service/starter/ThingsServiceGlobalErrorRegistryTest.java
+++ b/things/service/src/test/java/org/eclipse/ditto/things/service/starter/ThingsServiceGlobalErrorRegistryTest.java
@@ -35,6 +35,7 @@
 import org.eclipse.ditto.thingsearch.api.QueryTimeExceededException;
 import org.eclipse.ditto.thingsearch.model.signals.commands.exceptions.InvalidOptionException;
 import org.eclipse.ditto.wot.model.WotThingModelInvalidException;
+import org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException;
 
 public final class ThingsServiceGlobalErrorRegistryTest extends GlobalErrorRegistryTestCases {
 
@@ -60,7 +61,8 @@ public ThingsServiceGlobalErrorRegistryTest() {
                 PathUnknownException.class,
                 WotThingModelInvalidException.class,
                 InvalidOptionException.class,
-                QueryTimeExceededException.class
+                QueryTimeExceededException.class,
+                WotThingModelPayloadValidationException.class
         );
 
     }
diff --git a/things/service/src/test/resources/test.conf b/things/service/src/test/resources/test.conf
index f0da977556..c93ac279db 100755
--- a/things/service/src/test/resources/test.conf
+++ b/things/service/src/test/resources/test.conf
@@ -38,6 +38,12 @@ ditto {
       cache {
         maximum-size = 10
       }
+      tm-model-validation {
+        enabled = false
+        dynamic-configuration = [
+
+        ]
+      }
     }
   }
 
diff --git a/thingsearch/service/src/main/resources/search.conf b/thingsearch/service/src/main/resources/search.conf
index 945f66f6f0..0212a9d771 100755
--- a/thingsearch/service/src/main/resources/search.conf
+++ b/thingsearch/service/src/main/resources/search.conf
@@ -153,17 +153,21 @@ ditto {
         }
 
         throttle {
+          # Maximum number of PIDs to check per throttle `period`, and the maximum number of snapshots to read in one batch
           throughput = 100
           throughput = ${?BACKGROUND_SYNC_THROTTLE_THROUGHPUT}
 
+
           period = 10s
           period = ${?BACKGROUND_SYCN_THROTTLE_PERIOD}
         }
 
         # handle failures/stalling/expired cursors
+        # Minimum backoff in case of stream failure.
         min-backoff = 1s
         min-backoff = ${?BACKGROUND_SYNC_MIN_BACKOFF}
 
+        # Maximum backoff in case of stream failure
         max-backoff = 2m
         max-backoff = ${?BACKGROUND_SYNC_MAX_BACKOFF}
 
@@ -362,12 +366,12 @@ pekko {
 search-dispatcher {
   # one thread per query and a dedicated thread for the search actor
   type = PinnedDispatcher
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
 }
 
 blocked-namespaces-dispatcher {
   type = Dispatcher
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedForkJoinExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
   fork-join-executor {
     # Min number of threads to cap factor-based parallelism number to
     parallelism-min = 4
@@ -382,12 +386,12 @@ blocked-namespaces-dispatcher {
 
 policy-enforcer-cache-dispatcher {
   type = Dispatcher
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
 }
 
 thing-cache-dispatcher {
   type = Dispatcher
-  executor = "org.eclipse.ditto.internal.utils.metrics.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
+  executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
 }
 
 include "search-extension.conf"
diff --git a/thingsearch/service/src/test/resources/background-sync-test.conf b/thingsearch/service/src/test/resources/background-sync-test.conf
index 09cdac8bc5..5422cf1b75 100644
--- a/thingsearch/service/src/test/resources/background-sync-test.conf
+++ b/thingsearch/service/src/test/resources/background-sync-test.conf
@@ -1,5 +1,5 @@
 enabled = true
-quiet-period = 0s
+quiet-period = 100ms
 idle-timeout = 500ms
 keep {
   events = 200
diff --git a/wot/README.md b/wot/README.md
index 8642361153..a727c5a88b 100755
--- a/wot/README.md
+++ b/wot/README.md
@@ -5,12 +5,23 @@ This module contains models and implementation of the W3C "Web of Things" (WoT)
 As of version `2.4.0` of Ditto, this implementation follows the 
 [Web of Things (WoT) Thing Description 1.1](https://www.w3.org/TR/wot-thing-description11/).
 
-This module is separated in 2 submodules:
-* **model** (ditto-wot-model): contains a Java model of WoT (Web of Things) entities based on **ditto-json**.
+As of Ditto version `3.6.0`, the WoT integration was structured better to foster reusability of e.g. **model** and **api** +from codebases, reducing the dependencies on the WoT integration a lot. + +This WoT module is separated in the following submodules: +* **model** (`ditto-wot-model`): contains a Java model of WoT (Web of Things) entities based on **ditto-json**.
May be used in order to: * read a WoT "Thing Model" or "Thing Description" JSON from a String and convert it to Java objects * use the builder based Java API in order to create a WoT "Thing Model" or "Thing Description" from Java and e.g. output the JSON representation -* **integration** (ditto-wot-integration): contains interfaces and implementation of how WoT "Thing Models" are - converted to "Thing Descriptions" by e.g. injecting Ditto specific endpoints in the `form` definitions of the TDs - (Thing Descriptions) +* **api** (`ditto-wot-api`): contains API and implementation for the main functionality Ditto performs with regard to WoT: + * fetching a WoT TM (Thing Model) from an HTTP endpoint: `WotThingModelFetcher` + * requiring an implementation of the interface `JsonDownloader` + * resolving WoT TM (Thing Model) extensions and references: `WotThingModelExtensionResolver` + * generating Ditto `Thing` JSON skeletons based on a WoT TM: `WotThingSkeletonGenerator` + * generating Ditto specific WoT TDs (Thing Descriptions), including generated HTTP `forms` based on WoT TMs: `WotThingDescriptionGenerator` + * validating Ditto `Thing` instances against a WoT TM: `WotThingModelValidator` +* **validation** (`ditto-wot-validation`): contains configuration and logic how Ditto `Thing`s and `Feature`s should + be validated against WoT TMs (Thing Models), using a JsonSchema validation library, producing a `WotThingModelPayloadValidationException` + if validation against a given model was not successful +* **integration** (`ditto-wot-integration`): contains Ditto (and Apache Pekko) specific integration of the `ditto-wot-api` diff --git a/wot/api/pom.xml b/wot/api/pom.xml new file mode 100755 index 0000000000..1a486e08e7 --- /dev/null +++ b/wot/api/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + + org.eclipse.ditto + ditto-wot + ${revision} + + + ditto-wot-api + Eclipse Ditto :: WoT :: API + + + + org.eclipse.ditto + ditto-json + + + org.eclipse.ditto + ditto-base-model + + + org.eclipse.ditto + ditto-wot-model + + + org.eclipse.ditto + ditto-wot-validation + + + org.eclipse.ditto + ditto-things-model + + + + org.eclipse.ditto + ditto-internal-utils-cache + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-banned-dependencies + + enforce + + + + + + + org.apache.pekko + + + + true + + + + + + + diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultFeatureValidationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultFeatureValidationConfig.java new file mode 100644 index 0000000000..7603dcc1b3 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultFeatureValidationConfig.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024 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.wot.api.config; + +import java.util.Objects; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; +import org.eclipse.ditto.internal.utils.config.ScopedConfig; +import org.eclipse.ditto.wot.validation.config.FeatureValidationConfig; + +import com.typesafe.config.Config; + +/** + * This class is the default implementation of the WoT (Web of Things) {@link org.eclipse.ditto.wot.validation.config.FeatureValidationConfig}. + */ +@Immutable +final class DefaultFeatureValidationConfig implements FeatureValidationConfig { + + private static final String CONFIG_PATH = "feature"; + + private final boolean enforceFeatureDescriptionModification; + private final boolean enforcePresenceOfModeledFeatures; + private final boolean enforceProperties; + private final boolean enforceDesiredProperties; + private final boolean enforceInboxMessagesInput; + private final boolean enforceInboxMessagesOutput; + private final boolean enforceOutboxMessages; + private final boolean forbidFeatureDescriptionDeletion; + private final boolean forbidNonModeledFeatures; + private final boolean forbidNonModeledProperties; + private final boolean forbidNonModeledDesiredProperties; + private final boolean forbidNonModeledInboxMessages; + private final boolean forbidNonModeledOutboxMessages; + + private DefaultFeatureValidationConfig(final ScopedConfig scopedConfig) { + enforceFeatureDescriptionModification = + scopedConfig.getBoolean(ConfigValue.ENFORCE_FEATURE_DESCRIPTION_MODIFICATION.getConfigPath()); + enforcePresenceOfModeledFeatures = + scopedConfig.getBoolean(ConfigValue.ENFORCE_PRESENCE_OF_MODELED_FEATURES.getConfigPath()); + enforceProperties = + scopedConfig.getBoolean(ConfigValue.ENFORCE_PROPERTIES.getConfigPath()); + enforceDesiredProperties = + scopedConfig.getBoolean(ConfigValue.ENFORCE_DESIRED_PROPERTIES.getConfigPath()); + enforceInboxMessagesInput = + scopedConfig.getBoolean(ConfigValue.ENFORCE_INBOX_MESSAGES_INPUT.getConfigPath()); + enforceInboxMessagesOutput = + scopedConfig.getBoolean(ConfigValue.ENFORCE_INBOX_MESSAGES_OUTPUT.getConfigPath()); + enforceOutboxMessages = + scopedConfig.getBoolean(ConfigValue.ENFORCE_OUTBOX_MESSAGES.getConfigPath()); + forbidFeatureDescriptionDeletion = + scopedConfig.getBoolean(ConfigValue.FORBID_FEATURE_DESCRIPTION_DELETION.getConfigPath()); + forbidNonModeledFeatures = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_FEATURES.getConfigPath()); + forbidNonModeledProperties = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_PROPERTIES.getConfigPath()); + forbidNonModeledDesiredProperties = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_DESIRED_PROPERTIES.getConfigPath()); + forbidNonModeledInboxMessages = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_INBOX_MESSAGES.getConfigPath()); + forbidNonModeledOutboxMessages = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_OUTBOX_MESSAGES.getConfigPath()); + } + + /** + * Returns an instance of the thing config based on the settings of the specified Config. + * + * @param config is supposed to provide the settings of the thing config at {@value #CONFIG_PATH}. + * @return the instance. + * @throws org.eclipse.ditto.internal.utils.config.DittoConfigError if {@code config} is invalid. + */ + public static DefaultFeatureValidationConfig of(final Config config) { + return new DefaultFeatureValidationConfig(ConfigWithFallback.newInstance(config, CONFIG_PATH, + ConfigValue.values())); + } + + @Override + public boolean isEnforceFeatureDescriptionModification() { + return enforceFeatureDescriptionModification; + } + + @Override + public boolean isEnforcePresenceOfModeledFeatures() { + return enforcePresenceOfModeledFeatures; + } + + @Override + public boolean isEnforceProperties() { + return enforceProperties; + } + + @Override + public boolean isEnforceDesiredProperties() { + return enforceDesiredProperties; + } + + @Override + public boolean isEnforceInboxMessagesInput() { + return enforceInboxMessagesInput; + } + + @Override + public boolean isEnforceInboxMessagesOutput() { + return enforceInboxMessagesOutput; + } + + @Override + public boolean isEnforceOutboxMessages() { + return enforceOutboxMessages; + } + + @Override + public boolean isForbidFeatureDescriptionDeletion() { + return forbidFeatureDescriptionDeletion; + } + + @Override + public boolean isForbidNonModeledFeatures() { + return forbidNonModeledFeatures; + } + + @Override + public boolean isForbidNonModeledProperties() { + return forbidNonModeledProperties; + } + + @Override + public boolean isForbidNonModeledDesiredProperties() { + return forbidNonModeledDesiredProperties; + } + + @Override + public boolean isForbidNonModeledInboxMessages() { + return forbidNonModeledInboxMessages; + } + + @Override + public boolean isForbidNonModeledOutboxMessages() { + return forbidNonModeledOutboxMessages; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DefaultFeatureValidationConfig that = (DefaultFeatureValidationConfig) o; + return enforcePresenceOfModeledFeatures == that.enforcePresenceOfModeledFeatures && + forbidFeatureDescriptionDeletion == that.forbidFeatureDescriptionDeletion && + forbidNonModeledFeatures == that.forbidNonModeledFeatures && + enforceProperties == that.enforceProperties && + forbidNonModeledProperties == that.forbidNonModeledProperties && + enforceDesiredProperties == that.enforceDesiredProperties && + forbidNonModeledDesiredProperties == that.forbidNonModeledDesiredProperties && + enforceInboxMessagesInput == that.enforceInboxMessagesInput && + forbidNonModeledInboxMessages == that.forbidNonModeledInboxMessages && + enforceOutboxMessages == that.enforceOutboxMessages && + forbidNonModeledOutboxMessages == that.forbidNonModeledOutboxMessages; + } + + @Override + public int hashCode() { + return Objects.hash(enforcePresenceOfModeledFeatures, forbidFeatureDescriptionDeletion, + forbidNonModeledFeatures, enforceProperties, forbidNonModeledProperties, enforceDesiredProperties, + forbidNonModeledDesiredProperties, enforceInboxMessagesInput, enforceInboxMessagesOutput, + forbidNonModeledInboxMessages, enforceOutboxMessages, forbidNonModeledOutboxMessages); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "enforcePresenceOfModeledFeatures=" + enforcePresenceOfModeledFeatures + + ", enforceProperties=" + enforceProperties + + ", enforceDesiredProperties=" + enforceDesiredProperties + + ", enforceInboxMessagesInput=" + enforceInboxMessagesInput + + ", enforceInboxMessagesOutput=" + enforceInboxMessagesOutput + + ", enforceOutboxMessages=" + enforceOutboxMessages + + ", forbidFeatureDescriptionDeletion=" + forbidFeatureDescriptionDeletion + + ", forbidNonModeledFeatures=" + forbidNonModeledFeatures + + ", forbidNonModeledProperties=" + forbidNonModeledProperties + + ", forbidNonModeledDesiredProperties=" + forbidNonModeledDesiredProperties + + ", forbidNonModeledInboxMessages=" + forbidNonModeledInboxMessages + + ", forbidNonModeledOutboxMessages=" + forbidNonModeledOutboxMessages + + "]"; + } +} diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultThingValidationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultThingValidationConfig.java new file mode 100644 index 0000000000..dc65074c38 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultThingValidationConfig.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2024 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.wot.api.config; + +import java.util.Objects; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; +import org.eclipse.ditto.internal.utils.config.ScopedConfig; +import org.eclipse.ditto.wot.validation.config.ThingValidationConfig; + +import com.typesafe.config.Config; + +/** + * This class is the default implementation of the WoT (Web of Things) {@link org.eclipse.ditto.wot.validation.config.ThingValidationConfig}. + */ +@Immutable +final class DefaultThingValidationConfig implements ThingValidationConfig { + + private static final String CONFIG_PATH = "thing"; + + private final boolean enforceThingDescriptionModification; + private final boolean enforceAttributes; + private final boolean enforceInboxMessagesInput; + private final boolean enforceInboxMessagesOutput; + private final boolean enforceOutboxMessages; + private final boolean forbidThingDescriptionDeletion; + private final boolean forbidNonModeledAttributes; + private final boolean forbidNonModeledInboxMessages; + private final boolean forbidNonModeledOutboxMessages; + + private DefaultThingValidationConfig(final ScopedConfig scopedConfig) { + enforceThingDescriptionModification = + scopedConfig.getBoolean(ConfigValue.ENFORCE_THING_DESCRIPTION_MODIFICATION.getConfigPath()); + enforceAttributes = + scopedConfig.getBoolean(ConfigValue.ENFORCE_ATTRIBUTES.getConfigPath()); + enforceInboxMessagesInput = + scopedConfig.getBoolean(ConfigValue.ENFORCE_INBOX_MESSAGES_INPUT.getConfigPath()); + enforceInboxMessagesOutput = + scopedConfig.getBoolean(ConfigValue.ENFORCE_INBOX_MESSAGES_OUTPUT.getConfigPath()); + enforceOutboxMessages = + scopedConfig.getBoolean(ConfigValue.ENFORCE_OUTBOX_MESSAGES.getConfigPath()); + forbidThingDescriptionDeletion = + scopedConfig.getBoolean(ConfigValue.FORBID_THING_DESCRIPTION_DELETION.getConfigPath()); + forbidNonModeledAttributes = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_ATTRIBUTES.getConfigPath()); + forbidNonModeledInboxMessages = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_INBOX_MESSAGES.getConfigPath()); + forbidNonModeledOutboxMessages = + scopedConfig.getBoolean(ConfigValue.FORBID_NON_MODELED_OUTBOX_MESSAGES.getConfigPath()); + } + + /** + * Returns an instance of the thing config based on the settings of the specified Config. + * + * @param config is supposed to provide the settings of the thing config at {@value #CONFIG_PATH}. + * @return the instance. + * @throws org.eclipse.ditto.internal.utils.config.DittoConfigError if {@code config} is invalid. + */ + public static DefaultThingValidationConfig of(final Config config) { + return new DefaultThingValidationConfig(ConfigWithFallback.newInstance(config, CONFIG_PATH, + ConfigValue.values())); + } + + @Override + public boolean isEnforceThingDescriptionModification() { + return enforceThingDescriptionModification; + } + + @Override + public boolean isEnforceAttributes() { + return enforceAttributes; + } + + @Override + public boolean isEnforceInboxMessagesInput() { + return enforceInboxMessagesInput; + } + + @Override + public boolean isEnforceInboxMessagesOutput() { + return enforceInboxMessagesOutput; + } + + @Override + public boolean isEnforceOutboxMessages() { + return enforceOutboxMessages; + } + + @Override + public boolean isForbidThingDescriptionDeletion() { + return forbidThingDescriptionDeletion; + } + + @Override + public boolean isForbidNonModeledAttributes() { + return forbidNonModeledAttributes; + } + + @Override + public boolean isForbidNonModeledInboxMessages() { + return forbidNonModeledInboxMessages; + } + + @Override + public boolean isForbidNonModeledOutboxMessages() { + return forbidNonModeledOutboxMessages; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DefaultThingValidationConfig that = (DefaultThingValidationConfig) o; + return enforceThingDescriptionModification == that.enforceThingDescriptionModification && + forbidThingDescriptionDeletion == that.forbidThingDescriptionDeletion && + enforceAttributes == that.enforceAttributes && + forbidNonModeledAttributes == that.forbidNonModeledAttributes && + enforceInboxMessagesInput == that.enforceInboxMessagesInput && + forbidNonModeledInboxMessages == that.forbidNonModeledInboxMessages && + enforceOutboxMessages == that.enforceOutboxMessages && + forbidNonModeledOutboxMessages == that.forbidNonModeledOutboxMessages; + } + + @Override + public int hashCode() { + return Objects.hash(enforceThingDescriptionModification, forbidThingDescriptionDeletion, enforceAttributes, + forbidNonModeledAttributes, enforceInboxMessagesInput, enforceInboxMessagesOutput, + forbidNonModeledInboxMessages, enforceOutboxMessages, forbidNonModeledOutboxMessages); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "enforceThingDescriptionModification=" + enforceThingDescriptionModification + + ", enforceAttributes=" + enforceAttributes + + ", enforceInboxMessagesInput=" + enforceInboxMessagesInput + + ", enforceInboxMessagesOutput=" + enforceInboxMessagesOutput + + ", enforceOutboxMessages=" + enforceOutboxMessages + + ", forbidThingDescriptionDeletion=" + forbidThingDescriptionDeletion + + ", forbidNonModeledAttributes=" + forbidNonModeledAttributes + + ", forbidNonModeledInboxMessages=" + forbidNonModeledInboxMessages + + ", forbidNonModeledOutboxMessages=" + forbidNonModeledOutboxMessages + + "]"; + } +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmBasedCreationConfig.java similarity index 96% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmBasedCreationConfig.java index 1039a202ef..25cefa6b87 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmBasedCreationConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import java.util.Objects; diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmScopedCreationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmScopedCreationConfig.java similarity index 96% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmScopedCreationConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmScopedCreationConfig.java index f56139c3a0..4f0640ea0e 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmScopedCreationConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmScopedCreationConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import java.util.Objects; diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java new file mode 100644 index 0000000000..372f28ee84 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024 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.wot.api.config; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; +import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; +import org.eclipse.ditto.internal.utils.config.ScopedConfig; +import org.eclipse.ditto.wot.validation.ValidationContext; +import org.eclipse.ditto.wot.validation.config.FeatureValidationConfig; +import org.eclipse.ditto.wot.validation.config.ThingValidationConfig; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +/** + * This class is the default implementation of the WoT (Web of Things) {@link org.eclipse.ditto.wot.validation.config.TmValidationConfig}. + */ +@Immutable +final class DefaultTmValidationConfig implements TmValidationConfig { + + private static final String CONFIG_PATH = "tm-model-validation"; + + private static final String CONFIG_KEY_DYNAMIC_CONFIGURATION = "dynamic-configuration"; + + private final ScopedConfig scopedConfig; + private final List dynamicTmValidationConfigurations; + + private final boolean enabled; + private final ThingValidationConfig thingValidationConfig; + private final FeatureValidationConfig featureValidationConfig; + + + private DefaultTmValidationConfig(final ScopedConfig scopedConfig, + final List dynamicTmValidationConfigurations, + @Nullable final ValidationContext context + ) { + this.scopedConfig = scopedConfig; + this.dynamicTmValidationConfigurations = dynamicTmValidationConfigurations; + + final Config effectiveConfig = dynamicTmValidationConfigurations.stream() + .flatMap(dynamicConfig -> dynamicConfig.calculateDynamicTmValidationConfigOverrides(context).stream()) + .reduce(ConfigFactory.empty(), (a, b) -> b.withFallback(a)) + .withFallback(scopedConfig.resolve()); + enabled = effectiveConfig.getBoolean(ConfigValue.ENABLED.getConfigPath()); + + thingValidationConfig = DefaultThingValidationConfig.of(effectiveConfig); + featureValidationConfig = DefaultFeatureValidationConfig.of(effectiveConfig); + } + + /** + * Returns an instance of the thing config based on the settings of the specified Config. + * + * @param config is supposed to provide the settings of the thing config at {@value #CONFIG_PATH}. + * @return the instance. + * @throws org.eclipse.ditto.internal.utils.config.DittoConfigError if {@code config} is invalid. + */ + public static DefaultTmValidationConfig of(final Config config) { + final List dynamicTmValidationConfigurations = + DefaultScopedConfig.newInstance(config, CONFIG_PATH) + .getConfigList(CONFIG_KEY_DYNAMIC_CONFIGURATION) + .stream() + .map(InternalDynamicTmValidationConfiguration::new) + .toList(); + return new DefaultTmValidationConfig(ConfigWithFallback.newInstance(config, CONFIG_PATH, + ConfigValue.values()), dynamicTmValidationConfigurations, null); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public ThingValidationConfig getThingValidationConfig() { + return thingValidationConfig; + } + + @Override + public FeatureValidationConfig getFeatureValidationConfig() { + return featureValidationConfig; + } + + @Override + public TmValidationConfig withValidationContext(@Nullable final ValidationContext context) { + if (dynamicTmValidationConfigurations.isEmpty()) { + return this; + } else { + return new DefaultTmValidationConfig(scopedConfig, dynamicTmValidationConfigurations, context); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DefaultTmValidationConfig that = (DefaultTmValidationConfig) o; + return Objects.equals(dynamicTmValidationConfigurations, that.dynamicTmValidationConfigurations) && + enabled == that.enabled && + Objects.equals(thingValidationConfig, that.thingValidationConfig) && + Objects.equals(featureValidationConfig, that.featureValidationConfig) && + Objects.equals(scopedConfig, that.scopedConfig); + } + + @Override + public int hashCode() { + return Objects.hash(dynamicTmValidationConfigurations, enabled, thingValidationConfig, featureValidationConfig, + scopedConfig); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "dynamicTmValidationConfiguration=" + dynamicTmValidationConfigurations + + ", enabled=" + enabled + + ", thingValidationConfig=" + thingValidationConfig + + ", featureValidationConfig=" + featureValidationConfig + + ", scopedConfig=" + scopedConfig + + "]"; + } +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultToThingDescriptionConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultToThingDescriptionConfig.java similarity index 97% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultToThingDescriptionConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultToThingDescriptionConfig.java index 172775dbd3..92582a4d4c 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultToThingDescriptionConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultToThingDescriptionConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import java.util.Map; import java.util.Objects; diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultWotConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultWotConfig.java similarity index 69% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultWotConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultWotConfig.java index 052dc3a7a5..fae2f81098 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultWotConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultWotConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,18 +10,21 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import java.util.Objects; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.base.service.config.http.DefaultHttpProxyConfig; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; import org.eclipse.ditto.internal.utils.cache.config.DefaultCacheConfig; import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; import org.eclipse.ditto.internal.utils.config.ScopedConfig; +import org.eclipse.ditto.internal.utils.config.http.DefaultHttpProxyBaseConfig; +import org.eclipse.ditto.internal.utils.config.http.HttpProxyBaseConfig; +import org.eclipse.ditto.wot.validation.ValidationContext; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; import com.typesafe.config.Config; @@ -33,18 +36,25 @@ @Immutable public final class DefaultWotConfig implements WotConfig { + /** + * The parent path of the "wot" config. + */ + public static final String WOT_PARENT_CONFIG_PATH = "things"; + private static final String CONFIG_PATH = "wot"; - private final HttpProxyConfig httpProxyConfig; + private final HttpProxyBaseConfig httpProxyConfig; private final CacheConfig cacheConfig; private final ToThingDescriptionConfig toThingDescriptionConfig; - private final DefaultTmBasedCreationConfig tmBasedCreationConfig; + private final TmBasedCreationConfig tmBasedCreationConfig; + private final TmValidationConfig tmValidationConfig; private DefaultWotConfig(final ScopedConfig scopedConfig) { - httpProxyConfig = DefaultHttpProxyConfig.ofHttpProxy(scopedConfig); + httpProxyConfig = DefaultHttpProxyBaseConfig.ofHttpProxy(scopedConfig); cacheConfig = DefaultCacheConfig.of(scopedConfig, "cache"); toThingDescriptionConfig = DefaultToThingDescriptionConfig.of(scopedConfig); tmBasedCreationConfig = DefaultTmBasedCreationConfig.of(scopedConfig); + tmValidationConfig = DefaultTmValidationConfig.of(scopedConfig); } /** @@ -59,7 +69,7 @@ public static DefaultWotConfig of(final Config config) { } @Override - public HttpProxyConfig getHttpProxyConfig() { + public HttpProxyBaseConfig getHttpProxyConfig() { return httpProxyConfig; } @@ -78,6 +88,16 @@ public TmBasedCreationConfig getCreationConfig() { return tmBasedCreationConfig; } + @Override + public TmValidationConfig getValidationConfig() { + return tmValidationConfig; + } + + @Override + public TmValidationConfig getValidationConfig(@Nullable final ValidationContext context) { + return tmValidationConfig.withValidationContext(context); + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -90,12 +110,13 @@ public boolean equals(final Object o) { return Objects.equals(httpProxyConfig, that.httpProxyConfig) && Objects.equals(cacheConfig, that.cacheConfig) && Objects.equals(toThingDescriptionConfig, that.toThingDescriptionConfig) && - Objects.equals(tmBasedCreationConfig, that.tmBasedCreationConfig); + Objects.equals(tmBasedCreationConfig, that.tmBasedCreationConfig) && + Objects.equals(tmValidationConfig, that.tmValidationConfig); } @Override public int hashCode() { - return Objects.hash(httpProxyConfig, cacheConfig, toThingDescriptionConfig, tmBasedCreationConfig); + return Objects.hash(httpProxyConfig, cacheConfig, toThingDescriptionConfig, tmBasedCreationConfig, tmValidationConfig); } @Override @@ -105,6 +126,7 @@ public String toString() { ", cacheConfig=" + cacheConfig + ", toThingDescriptionConfig=" + toThingDescriptionConfig + ", tmBasedCreationConfig=" + tmBasedCreationConfig + + ", tmValidationConfig=" + tmValidationConfig + "]"; } diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/InternalDynamicTmValidationConfiguration.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/InternalDynamicTmValidationConfiguration.java new file mode 100644 index 0000000000..a3b27ca6aa --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/InternalDynamicTmValidationConfiguration.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 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.wot.api.config; + +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.wot.validation.ValidationContext; + +import com.typesafe.config.Config; + +/** + * Internal helper in order to dynamically apply WoT ThingModel validation/enforcement configuration based on the + * {@link ValidationContext} for individual API requests. + */ +final class InternalDynamicTmValidationConfiguration { + + private static final String CONFIG_KEY_VALIDATION_CONTEXT = "validation-context"; + private static final String CONFIG_KEY_DITTO_HEADERS_PATTERNS = "ditto-headers-patterns"; + private static final String CONFIG_KEY_THING_DEFINITION_PATTERNS = "thing-definition-patterns"; + private static final String CONFIG_KEY_FEATURE_DEFINITION_PATTERNS = "feature-definition-patterns"; + private static final String CONFIG_KEY_CONFIG_OVERRIDES = "config-overrides"; + + private final DynamicValidationContextConfiguration dynamicValidationContextConfiguration; + private final Config configOverrides; + + InternalDynamicTmValidationConfiguration(final Config config) { + final Config validationContext = config.getConfig(CONFIG_KEY_VALIDATION_CONTEXT); + final List> parsedDittoHeadersPatterns = validationContext + .getConfigList(CONFIG_KEY_DITTO_HEADERS_PATTERNS) + .stream() + .map(c -> c.entrySet() + .stream() + .map(e -> new AbstractMap.SimpleEntry<>( + e.getKey(), + Pattern.compile(e.getValue().unwrapped().toString()) + ) + ) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ) + .toList(); + final List thingDefinitionPatterns = validationContext + .getStringList(CONFIG_KEY_THING_DEFINITION_PATTERNS) + .stream() + .map(Pattern::compile) + .toList(); + final List featureDefinitionPatterns = validationContext + .getStringList(CONFIG_KEY_FEATURE_DEFINITION_PATTERNS) + .stream() + .map(Pattern::compile) + .toList(); + + dynamicValidationContextConfiguration = new DynamicValidationContextConfiguration( + parsedDittoHeadersPatterns, + thingDefinitionPatterns, + featureDefinitionPatterns + ); + configOverrides = config.getConfig(CONFIG_KEY_CONFIG_OVERRIDES); + } + + /** + * Calculates an optional configuration override to apply for the given {@code validationContext} or an empty + * optional if there should not be specific configuration overrides for the given context. + * + * @param validationContext the context to potentially apply a configuration overwrite for + * @return the optional configuration override to apply for the given validation context + */ + Optional calculateDynamicTmValidationConfigOverrides( + @Nullable final ValidationContext validationContext + ) { + if (validationContext == null) { + return Optional.empty(); + } else { + final boolean thingDefinitionMatches = validationContext.thingDefinition() != null && + dynamicValidationContextConfiguration.thingDefinitionPatterns().stream() + // OR + .anyMatch(pattern -> pattern.matcher(validationContext.thingDefinition().toString()) + .matches()); + if (!dynamicValidationContextConfiguration.thingDefinitionPatterns().isEmpty() && !thingDefinitionMatches) { + return Optional.empty(); + } + + final boolean featureDefinitionMatches = validationContext.featureDefinition() != null && + dynamicValidationContextConfiguration.featureDefinitionPatterns().stream() + // OR + .anyMatch(pattern -> pattern.matcher(validationContext.featureDefinition().toString()) + .matches()); + if (!dynamicValidationContextConfiguration.featureDefinitionPatterns().isEmpty() && !featureDefinitionMatches) { + return Optional.empty(); + } + + final boolean dittoHeadersMatch = dynamicValidationContextConfiguration.dittoHeadersPatterns() + .stream() + // OR + .anyMatch(headersMap -> headersMap.entrySet().stream() + // AND + .allMatch(headerEntry -> + Optional.ofNullable(validationContext.dittoHeaders().get(headerEntry.getKey())) + .filter(headerValue -> headerEntry.getValue() + .matcher(headerValue) + .matches()) + .isPresent() + ) + ); + if (!dynamicValidationContextConfiguration.dittoHeadersPatterns().isEmpty() && !dittoHeadersMatch) { + return Optional.empty(); + } + + return Optional.ofNullable(configOverrides); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof final InternalDynamicTmValidationConfiguration that)) { + return false; + } + return Objects.equals(dynamicValidationContextConfiguration, that.dynamicValidationContextConfiguration) && + Objects.equals(configOverrides, that.configOverrides); + } + + @Override + public int hashCode() { + return Objects.hash(dynamicValidationContextConfiguration, configOverrides); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "dynamicValidationContextConfiguration=" + dynamicValidationContextConfiguration + + ", configOverrides=" + configOverrides + + "]"; + } + + record DynamicValidationContextConfiguration( + List> dittoHeadersPatterns, + List thingDefinitionPatterns, + List featureDefinitionPatterns + ) { + } +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/TmBasedCreationConfig.java similarity index 94% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/TmBasedCreationConfig.java index 564bb98c78..a4754ebb4f 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/TmBasedCreationConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import javax.annotation.concurrent.Immutable; diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmScopedCreationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/TmScopedCreationConfig.java similarity index 96% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmScopedCreationConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/TmScopedCreationConfig.java index 9735b76808..bdd1cb6f71 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmScopedCreationConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/TmScopedCreationConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import javax.annotation.concurrent.Immutable; diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/ToThingDescriptionConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/ToThingDescriptionConfig.java similarity index 96% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/ToThingDescriptionConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/ToThingDescriptionConfig.java index 9f591aebb6..d68183cc88 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/ToThingDescriptionConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/ToThingDescriptionConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; import java.util.Map; diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/WotConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/WotConfig.java similarity index 57% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/WotConfig.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/WotConfig.java index c36b357efd..dd87da9690 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/WotConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/WotConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,12 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.config; +package org.eclipse.ditto.wot.api.config; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.base.service.config.http.HttpProxyConfig; import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; +import org.eclipse.ditto.internal.utils.config.http.HttpProxyBaseConfig; +import org.eclipse.ditto.wot.validation.ValidationContext; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; /** * Provides configuration settings for WoT (Web of Things) integration. @@ -30,7 +33,7 @@ public interface WotConfig { * * @return configuration settings for the HTTP proxy. */ - HttpProxyConfig getHttpProxyConfig(); + HttpProxyBaseConfig getHttpProxyConfig(); /** * Returns the cache configuration to apply for caching downloaded WoT Thing Models. @@ -50,8 +53,23 @@ public interface WotConfig { /** * Returns configuration for WoT TM (ThingModel) based creation of Things and Features. * - * @return configuration for WoT TM (ThingModel) based creation of Things and Features. + * @return configuration for WoT TM (ThingModel) based creation of Things and Features. */ TmBasedCreationConfig getCreationConfig(); + /** + * @return configuration for WoT (Web of Things) integration regarding the validation of Things and Features + * based on their WoT ThingModels. + * @since 3.6.0 + */ + TmValidationConfig getValidationConfig(); + + /** + * @param context the ValidationContext to evaluate for determining config dynamically + * @return configuration for WoT (Web of Things) integration regarding the validation of Things and Features + * based on their WoT ThingModels. + * @since 3.6.0 + */ + TmValidationConfig getValidationConfig(@Nullable ValidationContext context); + } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/package-info.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/package-info.java similarity index 80% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/package-info.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/config/package-info.java index c8a84b7185..92fb86e927 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/package-info.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,7 +13,7 @@ /** * Configuration for the WoT (Web of Things) integration. - * @since 2.4.0 + * @since 3.6.0 */ @org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault -package org.eclipse.ditto.wot.integration.config; \ No newline at end of file +package org.eclipse.ditto.wot.api.config; \ No newline at end of file diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingDescriptionGenerator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java similarity index 86% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingDescriptionGenerator.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java index 0cc272c70d..c83900d6b2 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingDescriptionGenerator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.generator; +package org.eclipse.ditto.wot.api.generator; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; @@ -23,7 +23,9 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.regex.Matcher; @@ -35,13 +37,10 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import org.apache.pekko.actor.ActorSystem; import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition; import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.base.model.headers.contenttype.ContentType; -import org.eclipse.ditto.internal.utils.pekko.logging.DittoLogger; -import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonField; @@ -49,12 +48,15 @@ import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.DefinitionIdentifier; import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureDefinition; import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingDefinition; import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.wot.integration.config.ToThingDescriptionConfig; -import org.eclipse.ditto.wot.integration.config.WotConfig; -import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.config.ToThingDescriptionConfig; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; import org.eclipse.ditto.wot.model.Action; import org.eclipse.ditto.wot.model.ActionFormElement; import org.eclipse.ditto.wot.model.ActionForms; @@ -63,6 +65,7 @@ import org.eclipse.ditto.wot.model.BaseLink; import org.eclipse.ditto.wot.model.BooleanSchema; import org.eclipse.ditto.wot.model.Description; +import org.eclipse.ditto.wot.model.DittoWotExtension; import org.eclipse.ditto.wot.model.Event; import org.eclipse.ditto.wot.model.EventFormElement; import org.eclipse.ditto.wot.model.EventForms; @@ -91,13 +94,17 @@ import org.eclipse.ditto.wot.model.SinglePropertyFormElementOp; import org.eclipse.ditto.wot.model.SingleRootFormElementOp; import org.eclipse.ditto.wot.model.StringSchema; +import org.eclipse.ditto.wot.model.ThingDefinitionInvalidException; import org.eclipse.ditto.wot.model.ThingDescription; import org.eclipse.ditto.wot.model.ThingModel; import org.eclipse.ditto.wot.model.Title; import org.eclipse.ditto.wot.model.UriVariables; import org.eclipse.ditto.wot.model.Version; +import org.eclipse.ditto.wot.model.WotInternalErrorException; import org.eclipse.ditto.wot.model.WotThingModelInvalidException; import org.eclipse.ditto.wot.model.WotThingModelPlaceholderUnresolvedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Default Ditto specific implementation of {@link WotThingDescriptionGenerator}. @@ -105,8 +112,7 @@ @Immutable final class DefaultWotThingDescriptionGenerator implements WotThingDescriptionGenerator { - private static final DittoLogger LOGGER = - DittoLoggerFactory.getLogger(DefaultWotThingDescriptionGenerator.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultWotThingDescriptionGenerator.class); private static final String TM_PLACEHOLDER_PL_GROUP = "pl"; private static final Pattern TM_PLACEHOLDER_PATTERN = @@ -123,15 +129,134 @@ final class DefaultWotThingDescriptionGenerator implements WotThingDescriptionGe private static final String SCHEMA_DITTO_ERROR = "dittoError"; private static final String DITTO_FIELDS_URI_VARIABLE = "fields"; + private static final String MODEL_PLACEHOLDERS_KEY = "model-placeholders"; + private final ToThingDescriptionConfig toThingDescriptionConfig; - private final WotThingModelExtensionResolver thingModelExtensionResolver; + private final WotThingModelResolver thingModelResolver; + private final Executor executor; - DefaultWotThingDescriptionGenerator(final ActorSystem actorSystem, + DefaultWotThingDescriptionGenerator( final WotConfig wotConfig, - final WotThingModelFetcher thingModelFetcher) { + final WotThingModelResolver thingModelResolver, + final Executor executor) { this.toThingDescriptionConfig = checkNotNull(wotConfig, "wotConfig").getToThingDescriptionConfig(); - thingModelExtensionResolver = new DefaultWotThingModelExtensionResolver(thingModelFetcher, - actorSystem.dispatchers().lookup("wot-dispatcher")); + this.thingModelResolver = checkNotNull(thingModelResolver, "thingModelResolver"); + this.executor = executor; + } + + @Override + public CompletionStage provideThingTD(@Nullable final ThingDefinition thingDefinition, + final ThingId thingId, + @Nullable final Thing thing, + final DittoHeaders dittoHeaders) { + if (null != thingDefinition) { + return getWotThingDescriptionForThing(thingDefinition, thingId, thing, dittoHeaders); + } else { + throw ThingDefinitionInvalidException.newBuilder(null) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + @Override + public CompletionStage provideFeatureTD(final ThingId thingId, + @Nullable final Thing thing, + final Feature feature, + final DittoHeaders dittoHeaders) { + + checkNotNull(feature, "feature"); + if (feature.getDefinition().isPresent()) { + return getWotThingDescriptionForFeature(thingId, thing, feature, dittoHeaders); + } else { + throw ThingDefinitionInvalidException.newBuilder(null) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + + + /** + * Download TM, add it to local cache + build TD + return it + */ + private CompletionStage getWotThingDescriptionForThing(final ThingDefinition definitionIdentifier, + final ThingId thingId, + @Nullable final Thing thing, + final DittoHeaders dittoHeaders) { + + final Optional urlOpt = definitionIdentifier.getUrl(); + if (urlOpt.isPresent()) { + final URL url = urlOpt.get(); + return thingModelResolver.resolveThingModel(url, dittoHeaders) + .thenComposeAsync(thingModel -> generateThingDescription(thingId, + thing, + Optional.ofNullable(thing) + .flatMap(Thing::getAttributes) + .flatMap(a -> a.getValue(MODEL_PLACEHOLDERS_KEY)) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .orElse(null), + null, + thingModel, + url, + dittoHeaders + ), + executor + ) + .exceptionally(throwable -> { + throw DittoRuntimeException.asDittoRuntimeException(throwable, t -> + WotInternalErrorException.newBuilder() + .dittoHeaders(dittoHeaders) + .cause(t) + .build()); + }); + } else { + throw ThingDefinitionInvalidException.newBuilder(definitionIdentifier) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + /** + * Download TM, add it to local cache + build TD + return it + */ + private CompletionStage getWotThingDescriptionForFeature(final ThingId thingId, + @Nullable final Thing thing, + final Feature feature, + final DittoHeaders dittoHeaders) { + + final Optional definitionIdentifier = feature.getDefinition() + .map(FeatureDefinition::getFirstIdentifier); + final Optional urlOpt = definitionIdentifier.flatMap(DefinitionIdentifier::getUrl); + if (urlOpt.isPresent()) { + final URL url = urlOpt.get(); + return thingModelResolver.resolveThingModel(url, dittoHeaders) + .thenComposeAsync(thingModel -> generateThingDescription(thingId, + thing, + feature.getProperties() + .flatMap(p -> p.getValue(MODEL_PLACEHOLDERS_KEY)) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .orElse(null), + feature.getId(), + thingModel, + url, + dittoHeaders + ), + executor + ) + .exceptionally(throwable -> { + throw DittoRuntimeException.asDittoRuntimeException(throwable, t -> + WotInternalErrorException.newBuilder() + .dittoHeaders(dittoHeaders) + .cause(t) + .build()); + }); + } else { + throw ThingDefinitionInvalidException.newBuilder(definitionIdentifier.orElse(null)) + .dittoHeaders(dittoHeaders) + .build(); + } } @Override @@ -144,15 +269,10 @@ public CompletionStage generateThingDescription(final ThingId final DittoHeaders dittoHeaders) { // generation rules defined at: https://w3c.github.io/wot-thing-description/#thing-model-td-generation - return thingModelExtensionResolver - .resolveThingModelExtensions(thingModel, dittoHeaders) - .thenCompose(thingModelWithExtensions -> - thingModelExtensionResolver.resolveThingModelRefs(thingModelWithExtensions, dittoHeaders) - ) + return CompletableFuture.completedFuture(thingModel) .thenApply(thingModelWithExtensionsAndImports -> { - LOGGER.withCorrelationId(dittoHeaders) - .debug("ThingModel after resolving extensions + refs: <{}>", - thingModelWithExtensionsAndImports); + LOGGER.debug("ThingModel after resolving extensions + refs: <{}>", + thingModelWithExtensionsAndImports); final ThingModel cleanedTm = removeThingModelSpecificElements(thingModelWithExtensionsAndImports, dittoHeaders); @@ -183,10 +303,8 @@ public CompletionStage generateThingDescription(final ThingId final ThingDescription thingDescription = resolvePlaceholders(tdBuilder.build(), placeholderLookupObject, dittoHeaders); - LOGGER.withCorrelationId(dittoHeaders) - .info("Created ThingDescription for thingId <{}> and featureId <{}>: <{}>", thingId, - featureId, - thingDescription); + LOGGER.info("Created ThingDescription for thingId <{}> and featureId <{}>: <{}>", thingId, + featureId, thingDescription); return thingDescription; }); } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingModelExtensionResolver.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingModelExtensionResolver.java similarity index 97% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingModelExtensionResolver.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingModelExtensionResolver.java index 631d2673d8..231c453e3c 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingModelExtensionResolver.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingModelExtensionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.generator; +package org.eclipse.ditto.wot.api.generator; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; @@ -27,7 +27,7 @@ import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; -import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.provider.WotThingModelFetcher; import org.eclipse.ditto.wot.model.IRI; import org.eclipse.ditto.wot.model.ThingModel; import org.eclipse.ditto.wot.model.WotThingModelRefInvalidException; @@ -43,8 +43,7 @@ final class DefaultWotThingModelExtensionResolver implements WotThingModelExtens private final WotThingModelFetcher thingModelFetcher; private final Executor executor; - DefaultWotThingModelExtensionResolver(final WotThingModelFetcher thingModelFetcher, - final Executor executor) { + DefaultWotThingModelExtensionResolver(final WotThingModelFetcher thingModelFetcher, final Executor executor) { this.thingModelFetcher = checkNotNull(thingModelFetcher, "thingModelFetcher"); this.executor = executor; } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java similarity index 67% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java index 32534431ff..af10fdb086 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,12 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.generator; +package org.eclipse.ditto.wot.api.generator; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; import java.net.MalformedURLException; import java.net.URL; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -27,16 +28,13 @@ import java.util.concurrent.Executor; import java.util.function.Function; import java.util.stream.IntStream; -import java.util.stream.Stream; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.japi.Pair; +import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; -import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; +import org.eclipse.ditto.base.model.signals.FeatureToggle; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonArrayBuilder; import org.eclipse.ditto.json.JsonObject; @@ -54,12 +52,15 @@ import org.eclipse.ditto.things.model.FeaturesBuilder; import org.eclipse.ditto.things.model.Thing; import org.eclipse.ditto.things.model.ThingBuilder; +import org.eclipse.ditto.things.model.ThingDefinition; import org.eclipse.ditto.things.model.ThingId; import org.eclipse.ditto.things.model.ThingsModelFactory; -import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; import org.eclipse.ditto.wot.model.ArraySchema; import org.eclipse.ditto.wot.model.BaseLink; import org.eclipse.ditto.wot.model.DataSchemaType; +import org.eclipse.ditto.wot.model.DittoWotExtension; import org.eclipse.ditto.wot.model.IRI; import org.eclipse.ditto.wot.model.IntegerSchema; import org.eclipse.ditto.wot.model.NumberSchema; @@ -67,11 +68,12 @@ import org.eclipse.ditto.wot.model.Properties; import org.eclipse.ditto.wot.model.Property; import org.eclipse.ditto.wot.model.SingleDataSchema; -import org.eclipse.ditto.wot.model.StringSchema; import org.eclipse.ditto.wot.model.ThingDefinitionInvalidException; import org.eclipse.ditto.wot.model.ThingModel; import org.eclipse.ditto.wot.model.TmOptional; -import org.eclipse.ditto.wot.model.WotThingModelInvalidException; +import org.eclipse.ditto.wot.model.WotInternalErrorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Default Ditto specific implementation of {@link WotThingSkeletonGenerator}. @@ -79,22 +81,131 @@ @Immutable final class DefaultWotThingSkeletonGenerator implements WotThingSkeletonGenerator { - private static final ThreadSafeDittoLogger LOGGER = - DittoLoggerFactory.getThreadSafeLogger(DefaultWotThingSkeletonGenerator.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultWotThingSkeletonGenerator.class); private static final String TM_EXTENDS = "tm:extends"; - private static final String TM_SUBMODEL = "tm:submodel"; - private static final String TM_SUBMODEL_INSTANCE_NAME = "instanceName"; - - private final WotThingModelFetcher thingModelFetcher; + private final WotConfig wotConfig; + private final WotThingModelResolver thingModelResolver; private final Executor executor; - private final WotThingModelExtensionResolver thingModelExtensionResolver; - DefaultWotThingSkeletonGenerator(final ActorSystem actorSystem, final WotThingModelFetcher thingModelFetcher) { - this.thingModelFetcher = checkNotNull(thingModelFetcher, "thingModelFetcher"); - executor = actorSystem.dispatchers().lookup("wot-dispatcher"); - thingModelExtensionResolver = new DefaultWotThingModelExtensionResolver(thingModelFetcher, executor); + DefaultWotThingSkeletonGenerator(final WotConfig wotConfig, + final WotThingModelResolver thingModelResolver, + final Executor executor) { + this.thingModelResolver = checkNotNull(thingModelResolver, "thingModelResolver"); + this.wotConfig = checkNotNull(wotConfig, "wotConfig"); + this.executor = executor; + } + + @Override + public CompletionStage> provideThingSkeletonForCreation(final ThingId thingId, + @Nullable final ThingDefinition thingDefinition, + final DittoHeaders dittoHeaders) { + + if (FeatureToggle.isWotIntegrationFeatureEnabled() && + wotConfig.getCreationConfig().getThingCreationConfig().isSkeletonCreationEnabled() && + null != thingDefinition) { + final Optional urlOpt = thingDefinition.getUrl(); + if (urlOpt.isPresent()) { + final URL url = urlOpt.get(); + LOGGER.debug("Resolving ThingModel from <{}> in order to create Thing skeleton for new Thing " + + "with id <{}>", url, thingId); + return thingModelResolver.resolveThingModel(url, dittoHeaders) + .thenComposeAsync(thingModel -> generateThingSkeleton( + thingId, + thingModel, + url, + wotConfig.getCreationConfig() + .getThingCreationConfig() + .shouldGenerateDefaultsForOptionalProperties(), + dittoHeaders + ), + executor + ) + .handle((thingSkeleton, throwable) -> { + if (throwable != null) { + LOGGER.info("Could not fetch ThingModel or generate Thing skeleton based on it due " + + "to: <{}: {}>", + throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); + if (wotConfig.getCreationConfig() + .getThingCreationConfig() + .shouldThrowExceptionOnWotErrors()) { + throw DittoRuntimeException.asDittoRuntimeException( + throwable, t -> WotInternalErrorException.newBuilder() + .dittoHeaders(dittoHeaders) + .cause(t) + .build() + ); + } else { + return Optional.empty(); + } + } else { + LOGGER.debug("Created Thing skeleton for new Thing with id <{}>: <{}>", thingId, + thingSkeleton); + return thingSkeleton; + } + }); + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } + } + + @Override + public CompletionStage> provideFeatureSkeletonForCreation(final String featureId, + @Nullable final FeatureDefinition featureDefinition, final DittoHeaders dittoHeaders) { + + if (FeatureToggle.isWotIntegrationFeatureEnabled() && + wotConfig.getCreationConfig().getFeatureCreationConfig().isSkeletonCreationEnabled() && + null != featureDefinition) { + final Optional urlOpt = featureDefinition.getFirstIdentifier().getUrl(); + if (urlOpt.isPresent()) { + final URL url = urlOpt.get(); + LOGGER.debug("Resolving ThingModel from <{}> in order to create Feature skeleton for new Feature " + + "with id <{}>", url, featureId); + return thingModelResolver.resolveThingModel(url, dittoHeaders) + .thenComposeAsync(thingModel -> generateFeatureSkeleton( + featureId, + thingModel, + url, + wotConfig.getCreationConfig() + .getFeatureCreationConfig() + .shouldGenerateDefaultsForOptionalProperties(), + dittoHeaders + ), + executor + ) + .handle((featureSkeleton, throwable) -> { + if (throwable != null) { + LOGGER.info("Could not fetch ThingModel or generate Feature skeleton based on it due " + + "to: <{}: {}>", + throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); + if (wotConfig.getCreationConfig() + .getFeatureCreationConfig() + .shouldThrowExceptionOnWotErrors()) { + throw DittoRuntimeException.asDittoRuntimeException( + throwable, t -> WotInternalErrorException.newBuilder() + .dittoHeaders(dittoHeaders) + .cause(t) + .build() + ); + } else { + return Optional.empty(); + } + } else { + LOGGER.debug("Created Feature skeleton for new Feature with id <{}>: <{}>", featureId, + featureSkeleton); + return featureSkeleton; + } + }); + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } } @Override @@ -104,18 +215,13 @@ public CompletionStage> generateThingSkeleton(final ThingId thin final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { - return thingModelExtensionResolver - .resolveThingModelExtensions(thingModel, dittoHeaders) - .thenCompose(thingModelWithExtensions -> - thingModelExtensionResolver.resolveThingModelRefs(thingModelWithExtensions, dittoHeaders) - ) + return CompletableFuture.completedFuture(thingModel) .thenApply(thingModelWithExtensionsAndImports -> { final Optional dittoExtensionPrefix = thingModelWithExtensionsAndImports.getAtContext() .determinePrefixFor(DittoWotExtension.DITTO_WOT_EXTENSION); - LOGGER.withCorrelationId(dittoHeaders) - .debug("ThingModel for generating Thing skeleton after resolving extensions + refs: <{}>", - thingModelWithExtensionsAndImports); + LOGGER.debug("ThingModel for generating Thing skeleton after resolving extensions + refs: <{}>", + thingModelWithExtensionsAndImports); final ThingBuilder.FromScratch builder = Thing.newBuilder(); thingModelWithExtensionsAndImports.getProperties() @@ -148,12 +254,12 @@ public CompletionStage> generateThingSkeleton(final ThingId thin return attributesBuilder.build(); }).ifPresent(builder::setAttributes); - return Pair.apply(thingModelWithExtensionsAndImports, builder); + return new AbstractMap.SimpleImmutableEntry<>(thingModelWithExtensionsAndImports, builder); }) .thenCompose(pair -> - createFeaturesFromSubmodels(pair.first(), generateDefaultsForOptionalProperties, dittoHeaders) + createFeaturesFromSubmodels(pair.getKey(), generateDefaultsForOptionalProperties, dittoHeaders) .thenApply(features -> - features.map(f -> pair.second().setFeatures(f)).orElse(pair.second()) + features.map(f -> pair.getValue().setFeatures(f)).orElse(pair.getValue()) ) ) .thenApply(builder -> Optional.of(builder.build())); @@ -195,55 +301,35 @@ private static void fillPropertiesInOptionalCategories(final Properties properti private CompletionStage> createFeaturesFromSubmodels(final ThingModel thingModel, final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { - final FeaturesBuilder featuresBuilder = Features.newBuilder(); - final List>> futureList = thingModel.getLinks() - .map(links -> links.stream() - .filter(baseLink -> baseLink.getRel().filter(TM_SUBMODEL::equals).isPresent()) - .map(baseLink -> { - final String instanceName = baseLink.getValue(TM_SUBMODEL_INSTANCE_NAME) - .filter(JsonValue::isString) - .map(JsonValue::asString) - .orElseThrow(() -> WotThingModelInvalidException - .newBuilder("The required 'instanceName' field of the " + - "'tm:submodel' link was not provided." - ).dittoHeaders(dittoHeaders) - .build() - ); - LOGGER.withCorrelationId(dittoHeaders) - .debug("Resolved TM submodel with instanceName <{}> and href <{}>", - instanceName, baseLink.getHref()); - return new Submodel(instanceName, baseLink.getHref()); - } - ) - ) - .orElseGet(Stream::empty) - .map(submodel -> thingModelFetcher.fetchThingModel(submodel.href, dittoHeaders) - .thenComposeAsync(subThingModel -> - generateFeatureSkeleton(submodel.instanceName, - subThingModel, - submodel.href, - generateDefaultsForOptionalProperties, - dittoHeaders - ), executor) - .toCompletableFuture() - ) - .toList(); + final CompletionStage>>> futureListStage = + thingModelResolver.resolveThingModelSubmodels(thingModel, dittoHeaders) + .thenApplyAsync(submodelMap -> submodelMap.entrySet().stream() + .map(entry -> generateFeatureSkeleton(entry.getKey().instanceName(), + entry.getValue(), + entry.getKey().href(), + generateDefaultsForOptionalProperties, + dittoHeaders + ).toCompletableFuture()) + .toList() + , executor); - return CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)) - .thenApplyAsync(v -> { - if (futureList.isEmpty()) { - return Optional.empty(); - } else { - featuresBuilder.setAll(futureList.stream() - .map(CompletableFuture::join) - .filter(Optional::isPresent) - .map(Optional::get) - .toList()); - return Optional.of(featuresBuilder.build()); - } - }, - executor - ); + final FeaturesBuilder featuresBuilder = Features.newBuilder(); + return futureListStage.thenCompose(futureList -> + CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)) + .thenApplyAsync(v -> { + if (futureList.isEmpty()) { + return Optional.empty(); + } else { + featuresBuilder.setAll(futureList.stream() + .map(CompletableFuture::join) + .filter(Optional::isPresent) + .map(Optional::get) + .toList()); + return Optional.of(featuresBuilder.build()); + } + }, + executor + )); } private CompletionStage> generateFeatureSkeleton(final String featureId, @@ -268,19 +354,16 @@ public CompletionStage> generateFeatureSkeleton(final String f final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { - return thingModelExtensionResolver - .resolveThingModelExtensions(thingModel, dittoHeaders) - .thenCompose(thingModelWithExtensions -> thingModelExtensionResolver - .resolveThingModelRefs(thingModelWithExtensions, dittoHeaders)) + return CompletableFuture.completedFuture(thingModel) .thenCombine(resolveFeatureDefinition(thingModel, thingModelUrl, dittoHeaders), (thingModelWithExtensionsAndImports, featureDefinition) -> { final Optional dittoExtensionPrefix = thingModelWithExtensionsAndImports.getAtContext() .determinePrefixFor(DittoWotExtension.DITTO_WOT_EXTENSION); - LOGGER.withCorrelationId(dittoHeaders) - .debug("ThingModel for generating Feature skeleton after resolving extensions + refs: <{}>", - thingModelWithExtensionsAndImports); + LOGGER.debug( + "ThingModel for generating Feature skeleton after resolving extensions + refs: <{}>", + thingModelWithExtensionsAndImports); final FeatureBuilder.FromScratchBuildable builder = Feature.newBuilder(); thingModelWithExtensionsAndImports.getProperties() @@ -409,9 +492,7 @@ private static Optional provideNeutralElementForDataSchema(final Sing numberSchema.getExclusiveMaximum().orElse(null)); return Optional.of(JsonValue.of(neutralDouble)); case STRING: - final StringSchema stringSchema = (StringSchema) dataSchema; - final String neutralString = provideNeutralStringElement(stringSchema.getMinLength().orElse(null)); - return Optional.of(JsonValue.of(neutralString)); + return Optional.of(JsonValue.of(provideNeutralStringElement())); case OBJECT: return Optional.of(JsonObject.empty()); case ARRAY: @@ -466,10 +547,7 @@ private static double provideNeutralDoubleElement(@Nullable final Double minimum return result; } - private static String provideNeutralStringElement(@Nullable final Integer minLength) { - if (null != minLength && minLength > 0) { - return "_".repeat(minLength); - } + private static String provideNeutralStringElement() { return ""; } @@ -493,7 +571,7 @@ private CompletionStage> determineFurtherFeatureDefin if (extendsLink.isPresent()) { final BaseLink link = extendsLink.get(); - return thingModelFetcher.fetchThingModel(link.getHref(), dittoHeaders) + return thingModelResolver.resolveThingModel(link.getHref(), dittoHeaders) .thenComposeAsync(subThingModel -> determineFurtherFeatureDefinitionIdentifiers( // recurse! subThingModel, @@ -512,14 +590,4 @@ private CompletionStage> determineFurtherFeatureDefin }).orElseGet(() -> CompletableFuture.completedFuture(Collections.emptyList())); } - private static class Submodel { - - private final String instanceName; - private final IRI href; - - public Submodel(final String instanceName, final IRI href) { - this.instanceName = instanceName; - this.href = href; - } - } } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/WotThingDescriptionProvider.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingDescriptionGenerator.java similarity index 50% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/WotThingDescriptionProvider.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingDescriptionGenerator.java index 3b7a7c83be..706d25dcdd 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/WotThingDescriptionProvider.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingDescriptionGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,34 +10,32 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.provider; +package org.eclipse.ditto.wot.api.generator; -import java.util.Optional; +import java.net.URL; import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.things.model.Feature; -import org.eclipse.ditto.things.model.FeatureDefinition; import org.eclipse.ditto.things.model.Thing; import org.eclipse.ditto.things.model.ThingDefinition; import org.eclipse.ditto.things.model.ThingId; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; import org.eclipse.ditto.wot.model.ThingDescription; - -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.actor.Extension; +import org.eclipse.ditto.wot.model.ThingModel; /** - * Extension for providing WoT (Web of Things) {@link ThingDescription}s for given {@code thingId}s from either a - * {@link ThingDefinition} or a {@link FeatureDefinition} from which the URL to a WoT - * {@link org.eclipse.ditto.wot.model.ThingModel} is resolved and the {@link ThingDescription} is provided. + * Generator for WoT (Web of Things) {@link ThingDescription} based on a given WoT {@link ThingModel} and context of the + * Ditto {@link Thing} to generate the ThingDescription for. * * @since 2.4.0 */ -@Immutable -public interface WotThingDescriptionProvider extends Extension { +public interface WotThingDescriptionGenerator { /** * Provides a {@link ThingDescription} for the given {@code thingDefinition} and {@code thingId} combination. @@ -78,44 +76,44 @@ CompletionStage provideFeatureTD(ThingId thingId, DittoHeaders dittoHeaders); /** - * Provides a {@link Thing} skeleton for the given {@code thingId} using the passed {@code thingDefinition} to - * extract a ThingModel URL from, fetching the ThingModel and generating the skeleton via - * {@link org.eclipse.ditto.wot.integration.generator.WotThingSkeletonGenerator}. - * The implementation should not throw exceptions, but return an empty optional if something went wrong during - * fetching or generation of the skeleton. + * Generates a ThingDescription for the given {@code thingId}, optionally using the passed {@code thing} to lookup + * thing specific placeholders. + * Uses the passed in {@code thingModel} and generates TD forms, security definition etc. in order to make it a + * valid TD. * - * @param thingId the ThingId to generate the Thing skeleton for. - * @param thingDefinition the ThingDefinition to resolve the ThingModel URL from. - * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. - * @return an optional Thing skeleton or empty optional if something went wrong during the skeleton creation. + * @param thingId the ThingId to generate the ThingDescription for. + * @param thing the optional Thing from which to resolve metadata from. + * @param placeholderLookupObject the optional JsonObject to dynamically resolve placeholders from + * (e.g. a Thing or Feature). + * @param featureId the optional feature name if the TD should be generated for a certain feature of the Thing. + * @param thingModel the ThingModel to use as template for generating the TD. + * @param thingModelUrl the URL from which the ThingModel was fetched. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeException which might occur during the + * generation. + * @return the generated ThingDescription for the given {@code thingId} based on the passed in {@code thingModel}. + * @throws org.eclipse.ditto.wot.model.WotThingModelInvalidException if the WoT ThingModel did not contain the + * mandatory {@code "@type"} being {@code "tm:ThingModel"} */ - CompletionStage> provideThingSkeletonForCreation(ThingId thingId, - @Nullable ThingDefinition thingDefinition, - DittoHeaders dittoHeaders); - - /** - * Provides a {@link Feature} skeleton for the given {@code featureId} using the passed {@code featureDefinition} to - * extract a ThingModel URL from, fetching the ThingModel and generating the skeleton via - * {@link org.eclipse.ditto.wot.integration.generator.WotThingSkeletonGenerator}. - * The implementation should not throw exceptions, but return an empty optional if something went wrong during - * fetching or generation of the skeleton. - * - * @param featureId the FeatureId to generate the Feature skeleton for. - * @param featureDefinition the FeatureDefinition to resolve the ThingModel URL from. - * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. - * @return an optional Feature skeleton or empty optional if something went wrong during the skeleton creation. - */ - CompletionStage> provideFeatureSkeletonForCreation(String featureId, - @Nullable FeatureDefinition featureDefinition, + CompletionStage generateThingDescription(ThingId thingId, + @Nullable Thing thing, + @Nullable JsonObject placeholderLookupObject, + @Nullable String featureId, + ThingModel thingModel, + URL thingModelUrl, DittoHeaders dittoHeaders); /** - * Get the {@code WotThingDescriptionProvider} for an actor system. + * Creates a new instance of WotThingDescriptionGenerator with the given {@code wotConfig}. * - * @param system the actor system. - * @return the {@code WotThingDescriptionProvider} extension. + * @param wotConfig the WoTConfig to use for creating the generator. + * @param thingModelResolver the ThingModel resolver to fetch and resolve (extensions, refs) of linked other + * ThingModels during the generation process. + * @param executor the executor to use to run async tasks. + * @return the created WotThingDescriptionGenerator. */ - static WotThingDescriptionProvider get(final ActorSystem system) { - return DefaultWotThingDescriptionProvider.ExtensionId.INSTANCE.get(system); + static WotThingDescriptionGenerator of(final WotConfig wotConfig, + final WotThingModelResolver thingModelResolver, + final Executor executor) { + return new DefaultWotThingDescriptionGenerator(wotConfig, thingModelResolver, executor); } } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingModelExtensionResolver.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingModelExtensionResolver.java similarity index 87% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingModelExtensionResolver.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingModelExtensionResolver.java index 38349e5409..c0aee200dd 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingModelExtensionResolver.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingModelExtensionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,13 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.generator; +package org.eclipse.ditto.wot.api.generator; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.provider.WotThingModelFetcher; import org.eclipse.ditto.wot.model.ThingModel; /** @@ -60,13 +60,16 @@ public interface WotThingModelExtensionResolver { CompletionStage resolveThingModelRefs(ThingModel thingModel, DittoHeaders dittoHeaders); /** - * Creates a new instance of WotThingModelExtensionResolver with the given {@code thingModelFetcher}. + * Creates a new instance of WotThingModelExtensionResolver with the given {@code thingModelFetcher} and + * {@code executor}. * * @param thingModelFetcher the ThingModel fetcher to fetch linked other ThingModels during the generation process. - * @param executor the executor to use for async tasks. - * @return the created WotThingSkeletonGenerator. + * @param executor the executor to use to run async tasks. + * @return the created WotThingModelExtensionResolver. + * @since 3.6.0 */ - static WotThingModelExtensionResolver of(final WotThingModelFetcher thingModelFetcher, final Executor executor) { - return new DefaultWotThingModelExtensionResolver(thingModelFetcher, executor); + static WotThingModelExtensionResolver of(final WotThingModelFetcher thingModelFetcher, + final Executor executor) { + return new DefaultWotThingModelExtensionResolver(thingModelFetcher,executor); } } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingSkeletonGenerator.java similarity index 69% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingSkeletonGenerator.java index c9c724d508..517bbadcf4 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/WotThingSkeletonGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,18 +10,23 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.generator; +package org.eclipse.ditto.wot.api.generator; import java.net.URL; import java.util.Optional; import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; -import org.apache.pekko.actor.ActorSystem; import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureDefinition; import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingDefinition; import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; import org.eclipse.ditto.wot.model.ThingModel; /** @@ -57,6 +62,23 @@ default CompletionStage> generateThingSkeleton(ThingId thingId, return generateThingSkeleton(thingId, thingModel, thingModelUrl, false, dittoHeaders); } + /** + * Provides a {@link Thing} skeleton for the given {@code thingId} using the passed {@code thingDefinition} to + * extract a ThingModel URL from, fetching the ThingModel and generating the skeleton via + * {@link WotThingSkeletonGenerator}. + * The implementation should not throw exceptions, but return an empty optional if something went wrong during + * fetching or generation of the skeleton. + * + * @param thingId the ThingId to generate the Thing skeleton for. + * @param thingDefinition the ThingDefinition to resolve the ThingModel URL from. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. + * @return an optional Thing skeleton or empty optional if something went wrong during the skeleton creation. + * @since 3.6.0 + */ + CompletionStage> provideThingSkeletonForCreation(ThingId thingId, + @Nullable ThingDefinition thingDefinition, + DittoHeaders dittoHeaders); + /** * Generates a skeleton {@link Thing} for the given {@code thingId}. * Uses the passed in {@code thingModel} and generates @@ -107,6 +129,23 @@ default CompletionStage> generateFeatureSkeleton(String featur return generateFeatureSkeleton(featureId, thingModel, thingModelUrl, false, dittoHeaders); } + /** + * Provides a {@link Feature} skeleton for the given {@code featureId} using the passed {@code featureDefinition} to + * extract a ThingModel URL from, fetching the ThingModel and generating the skeleton via + * {@link WotThingSkeletonGenerator}. + * The implementation should not throw exceptions, but return an empty optional if something went wrong during + * fetching or generation of the skeleton. + * + * @param featureId the FeatureId to generate the Feature skeleton for. + * @param featureDefinition the FeatureDefinition to resolve the ThingModel URL from. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. + * @return an optional Feature skeleton or empty optional if something went wrong during the skeleton creation. + * @since 3.6.0 + */ + CompletionStage> provideFeatureSkeletonForCreation(String featureId, + @Nullable FeatureDefinition featureDefinition, + DittoHeaders dittoHeaders); + /** * Generates a skeleton {@link Feature} for the given {@code featureId}. * Uses the passed in {@code thingModel} and generates @@ -135,11 +174,15 @@ CompletionStage> generateFeatureSkeleton(String featureId, /** * Creates a new instance of WotThingSkeletonGenerator with the given {@code wotConfig}. * - * @param actorSystem the actor system to use. - * @param thingModelFetcher the ThingModel fetcher to fetch linked other ThingModels during the generation process. + * @param wotConfig the WoT Config to use for creating the generator. + * @param thingModelResolver the ThingModel resolver to fetch and resolve (extensions, refs) of linked other + * ThingModels during the generation process. + * @param executor the executor to use to run async tasks. * @return the created WotThingSkeletonGenerator. */ - static WotThingSkeletonGenerator of(final ActorSystem actorSystem, final WotThingModelFetcher thingModelFetcher) { - return new DefaultWotThingSkeletonGenerator(actorSystem, thingModelFetcher); + static WotThingSkeletonGenerator of(final WotConfig wotConfig, + final WotThingModelResolver thingModelResolver, + final Executor executor) { + return new DefaultWotThingSkeletonGenerator(wotConfig, thingModelResolver, executor); } } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/package-info.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/package-info.java similarity index 79% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/package-info.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/package-info.java index b37be650e0..bb7b5808ab 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/package-info.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,7 +13,7 @@ /** * The TM to TD Generator for the WoT (Web of Things) integration. - * @since 2.4.0 + * @since 3.6.0 */ @org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault -package org.eclipse.ditto.wot.integration.generator; \ No newline at end of file +package org.eclipse.ditto.wot.api.generator; \ No newline at end of file diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/DefaultWotThingModelFetcher.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/DefaultWotThingModelFetcher.java new file mode 100644 index 0000000000..7bb4ba7420 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/DefaultWotThingModelFetcher.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 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.wot.api.provider; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.internal.utils.cache.Cache; +import org.eclipse.ditto.internal.utils.cache.CacheFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.model.IRI; +import org.eclipse.ditto.wot.model.ThingDefinitionInvalidException; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.WotThingModelInvalidException; +import org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; + +/** + * Default implementation of {@link WotThingModelFetcher} which should be not Ditto specific. + */ +final class DefaultWotThingModelFetcher implements WotThingModelFetcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultWotThingModelFetcher.class); + + private static final Duration MAX_FETCH_MODEL_DURATION = Duration.ofSeconds(10); + + private final JsonDownloader jsonDownloader; + private final Cache thingModelCache; + + DefaultWotThingModelFetcher(final WotConfig wotConfig, + final JsonDownloader jsonDownloader, + final Executor cacheLoaderExecutor) { + this.jsonDownloader = jsonDownloader; + final AsyncCacheLoader loader = this::loadThingModelViaHttp; + thingModelCache = CacheFactory.createCache(loader, + wotConfig.getCacheConfig(), + "ditto_wot_thing_model_cache", + cacheLoaderExecutor + ); + } + + @Override + public CompletableFuture fetchThingModel(final IRI iri, final DittoHeaders dittoHeaders) { + try { + return fetchThingModel(new URL(iri.toString()), dittoHeaders); + } catch (final MalformedURLException e) { + throw ThingDefinitionInvalidException.newBuilder(iri) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + @Override + public CompletableFuture fetchThingModel(final URL url, final DittoHeaders dittoHeaders) { + LOGGER.debug("Fetching ThingModel (from cache or downloading as fallback) from URL: <{}>", url); + return thingModelCache.get(url) + .thenApply(optTm -> resolveThingModel(optTm.orElse(null), url, dittoHeaders)) + .orTimeout(MAX_FETCH_MODEL_DURATION.toSeconds(), TimeUnit.SECONDS); + } + + private ThingModel resolveThingModel(@Nullable final ThingModel thingModel, + final URL tmUrl, + final DittoHeaders dittoHeaders) { + if (null != thingModel) { + LOGGER.debug("Resolved ThingModel: <{}>", thingModel); + return thingModel; + } else { + throw WotThingModelNotAccessibleException.newBuilder(tmUrl) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + /* this method is used to asynchronously load the ThingModel into the cache */ + private CompletableFuture loadThingModelViaHttp(final URL url, final Executor executor) { + LOGGER.debug("Loading ThingModel from URL <{}>.", url); + final CompletionStage responseFuture = jsonDownloader.downloadJsonViaHttp(url, executor); + final CompletionStage thingModelFuture = responseFuture + .thenApply(ThingModel::fromJson) + .exceptionally(t -> { + LOGGER.warn("Failed to extract ThingModel from response because of <{}: {}>", + t.getClass().getSimpleName(), t.getMessage()); + throw WotThingModelInvalidException.newBuilder(url) + .cause(t) + .build(); + }); + return thingModelFuture.toCompletableFuture(); + } + +} diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/JsonDownloader.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/JsonDownloader.java new file mode 100644 index 0000000000..0d94f486e0 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/JsonDownloader.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 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.wot.api.provider; + +import java.net.URL; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; + +import org.eclipse.ditto.json.JsonObject; + +/** + * Provides functionality to asynchronously download a {@link JsonObject} from a given {@link URL}. + * + * @since 3.6.0 + */ +public interface JsonDownloader { + + /** + * Downloads from the given {@code url} the content as {@code JsonObject} and provides it asynchronously using the + * passed {@code executor}. + * + * @param url the URL to download the json object from. + * @param executor the executor to use. + * @return a CompletionStage of the downloaded {@code JsonObject}, which may also be failed exceptionally, e.g. + * if the resource could not be accessed or of the provided resource could not be parsed as a {@link JsonObject}. + */ + CompletionStage downloadJsonViaHttp(URL url, Executor executor); +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/WotThingModelFetcher.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/WotThingModelFetcher.java similarity index 73% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/WotThingModelFetcher.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/WotThingModelFetcher.java index bc9608a3c9..5c5023fcaf 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/WotThingModelFetcher.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/WotThingModelFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,14 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.provider; +package org.eclipse.ditto.wot.api.provider; import java.net.URL; import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; -import org.apache.pekko.actor.ActorSystem; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.wot.integration.config.WotConfig; +import org.eclipse.ditto.wot.api.config.WotConfig; import org.eclipse.ditto.wot.model.IRI; import org.eclipse.ditto.wot.model.ThingModel; @@ -45,7 +45,7 @@ public interface WotThingModelFetcher { CompletionStage fetchThingModel(IRI iri, DittoHeaders dittoHeaders); /** - * Fetches the ThingModel resource at the passed {@code iurlri}. + * Fetches the ThingModel resource at the passed {@code url}. * * @param url the URL from which to fetch the ThingModel. * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. @@ -58,13 +58,17 @@ public interface WotThingModelFetcher { CompletionStage fetchThingModel(URL url, DittoHeaders dittoHeaders); /** - * Creates a new instance of WotThingModelExtensionResolver with the given {@code actorSystem}. + * Creates a new instance of WotThingModelFetcher with the given {@code actorSystem} and {@code wotConfig}. * - * @param actorSystem the actor system to use. * @param wotConfig the WoTConfig to use for creating the generator. - * @return the created WotThingSkeletonGenerator. + * @param jsonDownloader the downloader to use to download a JsonObject from a given URL. + * @param cacheLoaderExecutor the executor to use to run async cache loading tasks. + * @return the created WotThingModelFetcher. + * @since 3.6.0 */ - static WotThingModelFetcher of(final ActorSystem actorSystem, final WotConfig wotConfig) { - return new DefaultWotThingModelFetcher(actorSystem, wotConfig); + static WotThingModelFetcher of(final WotConfig wotConfig, + final JsonDownloader jsonDownloader, + final Executor cacheLoaderExecutor) { + return new DefaultWotThingModelFetcher(wotConfig, jsonDownloader, cacheLoaderExecutor); } } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/package-info.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/package-info.java similarity index 80% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/package-info.java rename to wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/package-info.java index 0ab8bd7d38..e479d99dc3 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/package-info.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/provider/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,7 +13,7 @@ /** * The provider for ThingDescriptions the WoT in the (Web of Things) integration. - * @since 2.4.0 + * @since 3.6.0 */ @org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault -package org.eclipse.ditto.wot.integration.provider; \ No newline at end of file +package org.eclipse.ditto.wot.api.provider; \ No newline at end of file diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/DefaultWotThingModelResolver.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/DefaultWotThingModelResolver.java new file mode 100644 index 0000000000..fcca26ce59 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/DefaultWotThingModelResolver.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 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.wot.api.resolver; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.internal.utils.cache.Cache; +import org.eclipse.ditto.internal.utils.cache.CacheFactory; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.generator.WotThingModelExtensionResolver; +import org.eclipse.ditto.wot.api.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.model.IRI; +import org.eclipse.ditto.wot.model.ThingDefinitionInvalidException; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.WotThingModelInvalidException; +import org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; + +/** + * Default implementation of {@link WotThingModelResolver} which should be not Ditto specific. + */ +final class DefaultWotThingModelResolver implements WotThingModelResolver { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultWotThingModelResolver.class); + + private static final String TM_SUBMODEL = "tm:submodel"; + private static final String TM_SUBMODEL_INSTANCE_NAME = "instanceName"; + + private static final Duration MAX_RESOLVE_MODEL_DURATION = Duration.ofSeconds(12); + + private final WotThingModelFetcher thingModelFetcher; + private final WotThingModelExtensionResolver thingModelExtensionResolver; + private final Cache fullyResolvedThingModelCache; + + DefaultWotThingModelResolver(final WotConfig wotConfig, + final WotThingModelFetcher thingModelFetcher, + final WotThingModelExtensionResolver thingModelExtensionResolver, + final Executor cacheLoaderExecutor) { + this.thingModelFetcher = thingModelFetcher; + this.thingModelExtensionResolver = thingModelExtensionResolver; + final AsyncCacheLoader loader = this::loadThingModelViaHttp; + fullyResolvedThingModelCache = CacheFactory.createCache(loader, + wotConfig.getCacheConfig(), + "ditto_wot_fully_resolved_thing_model_cache", + cacheLoaderExecutor); + } + + @Override + public CompletableFuture resolveThingModel(final IRI iri, final DittoHeaders dittoHeaders) { + try { + return resolveThingModel(new URL(iri.toString()), dittoHeaders); + } catch (final MalformedURLException e) { + throw ThingDefinitionInvalidException.newBuilder(iri) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + @Override + public CompletableFuture resolveThingModel(final URL url, final DittoHeaders dittoHeaders) { + LOGGER.debug("Resolving ThingModel (from cache or downloading as fallback) from URL: <{}>", url); + return fullyResolvedThingModelCache.get(url) + .thenApply(optTm -> resolveThingModel(optTm.orElse(null), url, dittoHeaders)) + .orTimeout(MAX_RESOLVE_MODEL_DURATION.toSeconds(), TimeUnit.SECONDS); + } + + @Override + public CompletionStage> resolveThingModelSubmodels(final ThingModel thingModel, + final DittoHeaders dittoHeaders) { + + final List>> futureList = + thingModel.getLinks() + .map(links -> links.stream() + .filter(baseLink -> baseLink.getRel().filter(TM_SUBMODEL::equals).isPresent()) + .map(baseLink -> { + final String instanceName = baseLink.getValue(TM_SUBMODEL_INSTANCE_NAME) + .filter(JsonValue::isString) + .map(JsonValue::asString) + .orElseThrow(() -> WotThingModelInvalidException + .newBuilder("The required 'instanceName' field of the " + + "'tm:submodel' link was not provided." + ).dittoHeaders(dittoHeaders) + .build() + ); + LOGGER.debug("Resolved TM submodel with instanceName <{}> and href <{}>", + instanceName, baseLink.getHref()); + return new ThingSubmodel(instanceName, baseLink.getHref()); + } + ) + ) + .orElseGet(Stream::empty) + .map(submodel -> resolveThingModel(submodel.href(), dittoHeaders) + .thenApply(subThingModel -> + new AbstractMap.SimpleEntry<>(submodel, subThingModel) + ) + .toCompletableFuture() + ) + .toList(); + + return CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)) + .thenApplyAsync(aVoid -> futureList.stream() + .map(CompletableFuture::join) // joining does not block anything here as "allOf" already guaranteed that all futures are ready + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)) + ); + } + + private ThingModel resolveThingModel(@Nullable final ThingModel thingModel, + final URL tmUrl, + final DittoHeaders dittoHeaders) { + if (null != thingModel) { + LOGGER.debug("Fully Resolved ThingModel: <{}>", thingModel); + return thingModel; + } else { + throw WotThingModelNotAccessibleException.newBuilder(tmUrl) + .dittoHeaders(dittoHeaders) + .build(); + } + } + + /* this method is used to asynchronously load the ThingModel into the cache */ + private CompletableFuture loadThingModelViaHttp(final URL url, final Executor executor) { + LOGGER.debug("Loading ThingModel from URL <{}>.", url); + final DittoHeaders dittoHeaders = DittoHeaders.empty(); + return thingModelFetcher.fetchThingModel(url, dittoHeaders) + .thenComposeAsync(thingModel -> + thingModelExtensionResolver + .resolveThingModelExtensions(thingModel, dittoHeaders) + .thenCompose(thingModelWithExtensions -> + thingModelExtensionResolver.resolveThingModelRefs(thingModelWithExtensions, + dittoHeaders) + ), + executor + ) + .toCompletableFuture(); + } + +} diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/ThingSubmodel.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/ThingSubmodel.java new file mode 100644 index 0000000000..477d9d20d3 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/ThingSubmodel.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 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.wot.api.resolver; + +import org.eclipse.ditto.wot.model.IRI; + +/** + * Bundles the {@code instanceName} and the {@code href} of a {@code tm:submodel} contained in the links of a + * ThingModel. + * + * @param instanceName the instance name of the submodel, translates to the "feature ID" in Ditto + * @param href the link where the submodel's TM is defined + */ +public record ThingSubmodel(String instanceName, IRI href) { +} diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/WotThingModelResolver.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/WotThingModelResolver.java new file mode 100644 index 0000000000..ca7209c463 --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/WotThingModelResolver.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 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.wot.api.resolver; + +import java.net.URL; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.generator.WotThingModelExtensionResolver; +import org.eclipse.ditto.wot.api.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.model.IRI; +import org.eclipse.ditto.wot.model.ThingModel; + +/** + * Fetches WoT (Web of Things) ThingModels from {@code IRI}s/{@code URL}s, resolves extensions and references and + * caches the fully resolved ThingModel in order to not always resolve extensions and references. + * + * @since 3.6.0 + */ +public interface WotThingModelResolver { + + /** + * Fetches the ThingModel resource at the passed {@code iri} and resolves extensions and references, returning the + * fully resolved model. + * + * @param iri the IRI (URL) from which to fetch the ThingModel. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. + * @return a CompletionStage containing the fetched ThingModel or completed exceptionally with a + * {@link org.eclipse.ditto.wot.model.WotThingModelInvalidException} if the fetched ThingModel could not be + * parsed/interpreted as correct WoT ThingModel. + * @throws org.eclipse.ditto.wot.model.ThingDefinitionInvalidException if the passed {@code iri} did not contain a + * valid URL. + * @throws org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException if the ThingModel could not be + * fetched at the given {@code iri}. + */ + CompletionStage resolveThingModel(IRI iri, DittoHeaders dittoHeaders); + + /** + * Fetches the ThingModel resource at the passed {@code url} and resolves extensions and references, returning the + * fully resolved model. + * + * @param url the URL from which to fetch the ThingModel. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. + * @return a CompletionStage containing the fetched ThingModel or completed exceptionally with a + * {@link org.eclipse.ditto.wot.model.WotThingModelInvalidException} if the fetched ThingModel could not be + * parsed/interpreted as correct WoT ThingModel. + * @throws org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException if the ThingModel could not be + * fetched at the given {@code url}. + */ + CompletionStage resolveThingModel(URL url, DittoHeaders dittoHeaders); + + /** + * Fetches all submodels contained in the passed {@code thingModel}, including extensions and references, returning + * a Map of all submodels. + * + * @param thingModel the ThingModel to fetch submodels for. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeExceptions. + * @return a CompletionStage containing the fetched ThingModel submodels or completed exceptionally with a + * {@link org.eclipse.ditto.wot.model.WotThingModelInvalidException} if the fetched ThingModels could not be + * parsed/interpreted as correct WoT ThingModels. + * @throws org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException if one of the ThingModel submodels + * could not be fetched at its defined {@code url}. + */ + CompletionStage> resolveThingModelSubmodels(ThingModel thingModel, + DittoHeaders dittoHeaders); + + /** + * Creates a new instance of WotThingModelResolver with the given {@code actorSystem} and {@code wotConfig}. + * + * @param wotConfig the WoT Config to use for creating the resolver. + * @param thingModelFetcher the WoT ThingModel fetcher used to download/fetch TMs from URLs. + * @param thingModelExtensionResolver the WoT ThingModel extension and reference resolver used to resolve + * {@code tm:extends} and {@code tm:ref} constructs in ThingModels. + * @param cacheLoaderExecutor the executor to use to run async cache loading tasks. + * @return the created WotThingModelResolver. + */ + static WotThingModelResolver of(final WotConfig wotConfig, + final WotThingModelFetcher thingModelFetcher, + final WotThingModelExtensionResolver thingModelExtensionResolver, + final Executor cacheLoaderExecutor) { + return new DefaultWotThingModelResolver(wotConfig, thingModelFetcher, thingModelExtensionResolver, + cacheLoaderExecutor); + } +} diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/package-info.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/package-info.java new file mode 100644 index 0000000000..8679aa4d8a --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/resolver/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 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 + */ + +/** + * The resolve for loading and resolving (extensions and references in) ThingModels the WoT in the (Web of Things) + * integration. + * @since 3.6.0 + */ +@org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault +package org.eclipse.ditto.wot.api.resolver; \ No newline at end of file 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 new file mode 100644 index 0000000000..d21f233a2d --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java @@ -0,0 +1,776 @@ +/* + * Copyright (c) 2024 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.wot.api.validator; + +import static org.eclipse.ditto.wot.validation.ValidationContext.buildValidationContext; + +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.signals.FeatureToggle; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.things.model.DefinitionIdentifier; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureDefinition; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingDefinition; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.resolver.ThingSubmodel; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.validation.ValidationContext; +import org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException; +import org.eclipse.ditto.wot.validation.WotThingModelValidation; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; + +/** + * Default Ditto specific implementation of {@link WotThingModelValidator}. + */ +@Immutable +final class DefaultWotThingModelValidator implements WotThingModelValidator { + + private final WotConfig wotConfig; + private final WotThingModelResolver thingModelResolver; + private final Executor executor; + + DefaultWotThingModelValidator(final WotConfig wotConfig, + final WotThingModelResolver thingModelResolver, + final Executor executor) { + this.wotConfig = wotConfig; + this.thingModelResolver = thingModelResolver; + this.executor = executor; + } + + @Override + public CompletionStage validateThing(final Thing thing, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + return thing.getDefinition() + .map(thingDefinition -> validateThing(thingDefinition, thing, resourcePath, dittoHeaders)) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThing(@Nullable final ThingDefinition thingDefinition, + final Thing thing, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + doValidateThing(Optional.ofNullable(thingDefinition).orElseThrow(), + thingModel, thing, resourcePath, context, validationConfig + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThing(final ThingDefinition thingDefinition, + final ThingModel thingModel, + final Thing thing, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> + doValidateThing(thingDefinition, thingModel, thing, resourcePath, context, validationConfig) + ) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingDefinitionModification(final ThingDefinition thingDefinition, + final Thing thing, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .filter(validationConfig -> validationConfig.isEnabled() && + validationConfig.getThingValidationConfig().isEnforceThingDescriptionModification() + ) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + doValidateThing(thingDefinition, thingModel, thing, Thing.JsonFields.DEFINITION.getPointer(), + context, validationConfig) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingDefinitionDeletion(final ThingDefinition thingDefinition, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .filter(validationConfig -> validationConfig.isEnabled() && + validationConfig.getThingValidationConfig().isForbidThingDescriptionDeletion() + ) + .map(validationConfig -> + CompletableFuture.failedStage( + WotThingModelPayloadValidationException + .newBuilder("Deleting the Thing's is not allowed") + .dittoHeaders(dittoHeaders) + .build() + ) + ) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingAttributes(@Nullable final ThingDefinition thingDefinition, + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + doValidateThingAttributes(thingModel, attributes, resourcePath, context, validationConfig) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingAttributes(final ThingDefinition thingDefinition, + final ThingModel thingModel, + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> + doValidateThingAttributes(thingModel, attributes, resourcePath, context, validationConfig) + ) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingAttribute(@Nullable final ThingDefinition thingDefinition, + final JsonPointer attributePointer, + final JsonValue attributeValue, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + selectValidation(validationConfig) + .validateThingAttribute(thingModel, + attributePointer, attributeValue, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingScopedDeletion(@Nullable final ThingDefinition thingDefinition, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + thingModelResolver.resolveThingModelSubmodels(thingModel, context.dittoHeaders()) + .thenCompose(subModels -> + selectValidation(validationConfig) + .validateThingScopedDeletion(thingModel, + reduceSubmodelMapKeyToFeatureId(subModels), + resourcePath, + context + ) + ) + + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingActionInput(@Nullable final ThingDefinition thingDefinition, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + selectValidation(validationConfig) + .validateThingActionInput(thingModel, + messageSubject, inputPayload, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingActionOutput(@Nullable final ThingDefinition thingDefinition, + final String messageSubject, + @Nullable final JsonValue outputPayload, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + selectValidation(validationConfig) + .validateThingActionOutput(thingModel, + messageSubject, outputPayload, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateThingEventData(@Nullable final ThingDefinition thingDefinition, + final String messageSubject, + @Nullable final JsonValue dataPayload, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + selectValidation(validationConfig) + .validateThingEventData(thingModel, + messageSubject, dataPayload, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatures(@Nullable final ThingDefinition thingDefinition, + final Features features, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + doValidateFeatures(thingModel, features, resourcePath, context, validationConfig) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatures(final ThingDefinition thingDefinition, + final ThingModel thingModel, + final Features features, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> + doValidateFeatures(thingModel, features, resourcePath, context, validationConfig) + ) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeature(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final Feature feature, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> { + final Optional urlOpt = Optional.ofNullable(thingDefinition).flatMap(ThingDefinition::getUrl); + return urlOpt.map(url -> fetchResolveAndValidateWith(url, dittoHeaders, thingModel -> + doValidateFeature(thingModel, featureDefinition, + feature, resourcePath, context, validationConfig + ) + ) + ) + .orElseGet(() -> + doValidateFeature(null, featureDefinition, + feature, resourcePath, context, validationConfig + ) + ); + }) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeature(@Nullable final ThingDefinition thingDefinition, + @Nullable final ThingModel thingModel, + @Nullable final FeatureDefinition featureDefinition, + @Nullable final ThingModel featureThingModel, + final Feature feature, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> + doValidateFeature(thingModel, featureDefinition, featureThingModel, + feature, resourcePath, context, validationConfig + ) + ) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureDefinitionModification(@Nullable final ThingDefinition thingDefinition, + final FeatureDefinition featureDefinition, + final Feature feature, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .filter(validationConfig -> validationConfig.isEnabled() && + validationConfig.getFeatureValidationConfig().isEnforceFeatureDescriptionModification() + ) + .map(validationConfig -> fetchResolveAndValidateWith(featureDefinition.getFirstIdentifier(), + dittoHeaders, + featureThingModel -> + selectValidation(validationConfig) + .validateFeature(featureThingModel, feature, resourcePath, context) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureDefinitionDeletion(@Nullable final ThingDefinition thingDefinition, + final FeatureDefinition featureDefinition, + final String featureId, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return doValidateFeatureDefinitionDeletion(featureId, dittoHeaders, context); + } + + @Override + public CompletionStage validateFeatureProperties(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final String featureId, + @Nullable final FeatureProperties featureProperties, + final boolean desiredProperties, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith( + Optional.ofNullable(featureDefinition).map(FeatureDefinition::getFirstIdentifier).orElse(null), + dittoHeaders, + featureThingModelWithExtensionsAndImports -> + validateFeatureProperties(thingDefinition, + Optional.ofNullable(featureDefinition).orElseThrow(), + featureThingModelWithExtensionsAndImports, featureId, featureProperties, + desiredProperties, resourcePath, dittoHeaders + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureProperties(@Nullable final ThingDefinition thingDefinition, + final FeatureDefinition featureDefinition, + final ThingModel featureThingModel, + final String featureId, + @Nullable final FeatureProperties featureProperties, + final boolean desiredProperties, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> + selectValidation(validationConfig) + .validateFeatureProperties(featureThingModel, featureId, featureProperties, + desiredProperties, resourcePath, context + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureProperty(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final String featureId, + final JsonPointer propertyPointer, + final JsonValue propertyValue, + final boolean desiredProperty, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith( + Optional.ofNullable(featureDefinition).map(FeatureDefinition::getFirstIdentifier).orElse(null), + dittoHeaders, featureThingModel -> + selectValidation(validationConfig) + .validateFeatureProperty(featureThingModel, + featureId, propertyPointer, propertyValue, desiredProperty, + resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureScopedDeletion(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final String featureId, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> + thingModelResolver.resolveThingModelSubmodels(thingModel, context.dittoHeaders()) + .thenCompose(subModels -> + fetchResolveAndValidateWith( + Optional.ofNullable(featureDefinition) + .map(FeatureDefinition::getFirstIdentifier).orElse(null), + dittoHeaders, + featureThingModel -> + selectValidation(validationConfig) + .validateFeatureScopedDeletion( + reduceSubmodelMapKeyToFeatureId(subModels), + featureThingModel, featureId, resourcePath, + context + ) + ) + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureActionInput(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final String featureId, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith( + Optional.ofNullable(featureDefinition).map(FeatureDefinition::getFirstIdentifier).orElse(null), + dittoHeaders, featureThingModel -> + selectValidation(validationConfig) + .validateFeatureActionInput(featureThingModel, + featureId, messageSubject, inputPayload, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureActionOutput(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final String featureId, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith( + Optional.ofNullable(featureDefinition).map(FeatureDefinition::getFirstIdentifier).orElse(null), + dittoHeaders, featureThingModel -> + selectValidation(validationConfig) + .validateFeatureActionOutput(featureThingModel, + featureId, messageSubject, inputPayload, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + @Override + public CompletionStage validateFeatureEventData(@Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition, + final String featureId, + final String messageSubject, + @Nullable final JsonValue dataPayload, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final ValidationContext context = buildValidationContext(dittoHeaders, thingDefinition, featureDefinition); + return provideValidationConfigIfWotValidationEnabled(context) + .map(validationConfig -> fetchResolveAndValidateWith( + Optional.ofNullable(featureDefinition).map(FeatureDefinition::getFirstIdentifier).orElse(null), + dittoHeaders, featureThingModel -> + selectValidation(validationConfig) + .validateFeatureEventData(featureThingModel, + featureId, messageSubject, dataPayload, resourcePath, context + ) + )) + .orElseGet(DefaultWotThingModelValidator::success); + } + + private WotThingModelValidation selectValidation(final TmValidationConfig validationConfig) { + return WotThingModelValidation.of(validationConfig); + } + + private Optional provideValidationConfigIfWotValidationEnabled( + final ValidationContext context + ) { + final TmValidationConfig validationConfig = wotConfig.getValidationConfig(context); + if (FeatureToggle.isWotIntegrationFeatureEnabled() && validationConfig.isEnabled()) { + return Optional.of(validationConfig); + } else { + return Optional.empty(); + } + } + + private static CompletionStage success() { + return CompletableFuture.completedStage(null); + } + + private CompletionStage fetchResolveAndValidateWith(@Nullable final DefinitionIdentifier definitionIdentifier, + final DittoHeaders dittoHeaders, + final Function> validationFunction + ) { + final Optional urlOpt = Optional.ofNullable(definitionIdentifier).flatMap(DefinitionIdentifier::getUrl); + return urlOpt + .map(url -> fetchResolveAndValidateWith(url, dittoHeaders, validationFunction)) + .orElseGet(DefaultWotThingModelValidator::success); + } + + private CompletionStage fetchResolveAndValidateWith(final URL url, + final DittoHeaders dittoHeaders, + final Function> validationFunction + ) { + return thingModelResolver.resolveThingModel(url, dittoHeaders) + .thenComposeAsync(validationFunction, executor); + } + + private CompletionStage doValidateThing(final ThingDefinition thingDefinition, + final ThingModel thingModel, + final Thing thing, + final JsonPointer resourcePath, + final ValidationContext context, + final TmValidationConfig validationConfig + ) { + final CompletionStage firstStage; + if (validationConfig.getThingValidationConfig().isForbidThingDescriptionDeletion() && + thing.getDefinition().isEmpty()) { + firstStage = validateThingDefinitionDeletion(thingDefinition, context.dittoHeaders()); + } else { + firstStage = success(); + } + return firstStage.thenCompose(unused -> + doValidateThingAttributes(thingModel, + thing.getAttributes().orElse(null), + resourcePath.append(Thing.JsonFields.ATTRIBUTES.getPointer()), + context, + validationConfig + ) + ).thenCompose(aVoid -> + thingModelResolver.resolveThingModelSubmodels(thingModel, context.dittoHeaders()) + .thenCompose(subModels -> + doValidateFeatures(subModels, + thing.getFeatures().orElse(null), + resourcePath, + context, + validationConfig + ) + ) + ); + } + + private CompletionStage doValidateThingAttributes(final ThingModel thingModel, + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final ValidationContext context, + final TmValidationConfig validationConfig + ) { + return selectValidation(validationConfig) + .validateThingAttributes(thingModel, attributes, resourcePath, context); + } + + private CompletionStage doValidateFeatures(final ThingModel thingModel, + final Features features, + final JsonPointer resourcePath, + final ValidationContext context, + final TmValidationConfig validationConfig + ) { + return thingModelResolver.resolveThingModelSubmodels(thingModel, context.dittoHeaders()) + .thenCompose(subModels -> + doValidateFeatures(subModels, features, resourcePath, context, validationConfig) + ); + } + + private CompletionStage doValidateFeatures(final Map subModels, + @Nullable final Features features, + final JsonPointer resourcePath, + final ValidationContext context, + final TmValidationConfig validationConfig + ) { + final Map featureThingModels = reduceSubmodelMapKeyToFeatureId(subModels); + return selectValidation(validationConfig) + .validateFeaturesPresence(featureThingModels, features, context) + .thenCompose(aVoid -> CompletableFuture.allOf( + featureThingModels.entrySet() + .stream() + .map(entry -> { + final String featureId = entry.getKey(); + if (validationConfig.getFeatureValidationConfig() + .isForbidFeatureDescriptionDeletion() && + entry.getValue() != null && Optional.ofNullable(features) + .flatMap(fs -> fs.getFeature(featureId)) + .flatMap(Feature::getDefinition) + .isEmpty() + ) { + return doValidateFeatureDefinitionDeletion(featureId, + context.dittoHeaders(), context + ); + } else { + return success(); + } + }) + .map(CompletionStage::toCompletableFuture) + .toArray(CompletableFuture[]::new) + ) + ).thenCompose(aVoid -> + selectValidation(validationConfig) + .validateFeaturesProperties(featureThingModels, features, resourcePath, context) + ); + } + + private CompletionStage doValidateFeature(@Nullable final ThingModel thingModel, + @Nullable final FeatureDefinition previousFeatureDefinition, + final Feature feature, + final JsonPointer resourcePath, + final ValidationContext context, + final TmValidationConfig validationConfig + ) { + final Optional featureDefinition = feature.getDefinition(); + final Optional definitionIdentifier = featureDefinition + .map(FeatureDefinition::getFirstIdentifier); + final Optional urlOpt = definitionIdentifier.flatMap(DefinitionIdentifier::getUrl); + return urlOpt.map(url -> fetchResolveAndValidateWith(url, context.dittoHeaders(), featureThingModel -> + doValidateFeature(thingModel, previousFeatureDefinition, featureThingModel, + feature, resourcePath, context, validationConfig + ) + )) + .orElseGet(() -> + doValidateFeature(thingModel, previousFeatureDefinition, null, + feature, resourcePath, context, validationConfig + ) + ); + } + + private CompletionStage doValidateFeature(@Nullable final ThingModel thingModel, + @Nullable final FeatureDefinition previousFeatureDefinition, + @Nullable final ThingModel featureThingModel, + final Feature feature, + final JsonPointer resourcePath, + final ValidationContext context, + final TmValidationConfig validationConfig + ) { + final WotThingModelValidation selectedValidation = selectValidation(validationConfig); + final CompletionStage firstStage; + if (validationConfig.getFeatureValidationConfig().isForbidFeatureDescriptionDeletion() && + previousFeatureDefinition != null && feature.getDefinition().isEmpty() + ) { + firstStage = doValidateFeatureDefinitionDeletion( + feature.getId(), + context.dittoHeaders(), + context + ); + } else { + firstStage = CompletableFuture.completedStage(null); + } + + final CompletionStage secondStage; + if (thingModel != null && featureThingModel != null) { + secondStage = thingModelResolver.resolveThingModelSubmodels(thingModel, context.dittoHeaders()) + .thenCompose(subModels -> + selectedValidation.validateFeaturePresence( + reduceSubmodelMapKeyToFeatureId(subModels), feature, context + ).thenCompose(aVoid -> + selectedValidation.validateFeature(featureThingModel, feature, + resourcePath, context) + ) + ); + } else if (thingModel != null) { + return thingModelResolver.resolveThingModelSubmodels(thingModel, context.dittoHeaders()) + .thenCompose(subModels -> { + final Map featureThingModels = reduceSubmodelMapKeyToFeatureId(subModels); + return selectedValidation.validateFeaturePresence(featureThingModels, feature, context) + .thenCompose(aVoid -> + selectedValidation.validateFeature( + featureThingModels.get(feature.getId()), + feature, resourcePath, context + ) + ); + }); + } else if (featureThingModel != null) { + secondStage = selectedValidation.validateFeature(featureThingModel, feature, resourcePath, context); + } else { + secondStage = success(); + } + return firstStage.thenCompose(unused -> secondStage); + } + + private CompletionStage doValidateFeatureDefinitionDeletion(final String featureId, + final DittoHeaders dittoHeaders, + final ValidationContext context + ) { + return provideValidationConfigIfWotValidationEnabled(context) + .filter(validationConfig -> validationConfig.isEnabled() && + validationConfig.getFeatureValidationConfig().isForbidFeatureDescriptionDeletion() + ) + .map(validationConfig -> + CompletableFuture.failedStage( + WotThingModelPayloadValidationException + .newBuilder("Deleting the Feature <" + featureId + ">'s " + + " is not allowed") + .dittoHeaders(dittoHeaders) + .build() + ) + ) + .orElseGet(DefaultWotThingModelValidator::success); + } + + private static LinkedHashMap reduceSubmodelMapKeyToFeatureId( + final Map subModels + ) { + return subModels.entrySet().stream().collect( + Collectors.toMap( + e -> e.getKey().instanceName(), + Map.Entry::getValue, + (a, b) -> a, + LinkedHashMap::new + ) + ); + } +} 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 new file mode 100644 index 0000000000..a7aa8bfb8a --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2024 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.wot.api.validator; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureDefinition; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingDefinition; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; +import org.eclipse.ditto.wot.model.ThingModel; + +/** + * Validates different aspects of Ditto a {@link Thing} against a WoT {@link ThingModel} linked in the Thing's + * {@link ThingDefinition}: + *
    + *
  • Attributes
  • + *
  • Features
  • + *
  • Feature properties
  • + *
  • Feature desired properties
  • + *
  • Thing messages
  • + *
  • Feature messages
  • + *
+ * + * @since 3.6.0 + */ +public interface WotThingModelValidator { + + /** + * Validates the provided {@code thing} against its contained {@code thingDefinition} (if this links to a WoT TM). + * + * @param thing the Thing 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThing(Thing thing, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code thing} against the provided {@code thingDefinition} (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param thing the Thing 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThing(@Nullable ThingDefinition thingDefinition, + Thing thing, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code thing} against the provided {@code thingModel}. + * + * @param thingDefinition the ThingDefinition which was used to retrieve the passed {@code thingModel} + * @param thingModel the ThingModel to validate against + * @param thing the Thing 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThing(ThingDefinition thingDefinition, + ThingModel thingModel, + Thing thing, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code thing} in context of a modification of its {@code thingDefinition} + * (e.g. in order to update it to a new version). + * + * @param thingDefinition the new, updated ThingDefinition to retrieve the WoT TM from to validate against + * @param thing the Thing to validate + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingDefinitionModification(ThingDefinition thingDefinition, + Thing thing, + DittoHeaders dittoHeaders + ); + + /** + * Validates if the deletion of the Thing's {@code thingDefinition} is allowed to do. + * Does so by just checking the configuration. + * + * @param thingDefinition the ThingDefinition to delete + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingDefinitionDeletion(ThingDefinition thingDefinition, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code attributes} against on the provided {@code thingDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param attributes the attributes 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingAttributes(@Nullable ThingDefinition thingDefinition, + @Nullable Attributes attributes, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code attributes} against on the provided {@code thingModel}. + * + * @param thingDefinition the ThingDefinition which was used to retrieve the passed {@code thingModel} + * @param thingModel the ThingModel to validate against + * @param attributes the attributes 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingAttributes(ThingDefinition thingDefinition, + ThingModel thingModel, + @Nullable Attributes attributes, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code attribute} against on the provided {@code thingDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param attributePointer the pointer (path) of the attribute to validate + * @param attributeValue the attribute value 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingAttribute(@Nullable ThingDefinition thingDefinition, + JsonPointer attributePointer, + JsonValue attributeValue, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates a deletion inside a thing on "thing" scope, e.g. all attributes, a single attribute or all features. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingScopedDeletion(@Nullable ThingDefinition thingDefinition, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code inputPayload} of the inbox message (WoT action) with subject {@code messageSubject} + * against the provided {@code thingDefinition}. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param messageSubject the (Thing) message subject + * @param inputPayload 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingActionInput(@Nullable ThingDefinition thingDefinition, + String messageSubject, + @Nullable JsonValue inputPayload, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code outputPayload} of the inbox message response (response to WoT action) with subject + * {@code messageSubject} against the provided {@code thingDefinition}. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param messageSubject the (Thing) message subject + * @param outputPayload 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingActionOutput(@Nullable ThingDefinition thingDefinition, + String messageSubject, + @Nullable JsonValue outputPayload, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code dataPayload} of the outbox message (WoT event) with subject {@code messageSubject} + * against the provided {@code thingDefinition}. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param messageSubject the (Thing) message subject + * @param dataPayload the 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateThingEventData(@Nullable ThingDefinition thingDefinition, + String messageSubject, + @Nullable JsonValue dataPayload, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code features} of a Thing against the provided {@code thingDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param features the features 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatures(@Nullable ThingDefinition thingDefinition, + Features features, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code features} of a Thing against the provided {@code thingModel}. + * + * @param thingDefinition the ThingDefinition which was used to retrieve the passed {@code thingModel} + * @param thingModel the ThingModel to validate against + * @param features the features 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatures(ThingDefinition thingDefinition, + ThingModel thingModel, + Features features, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code feature} of a Thing against the provided {@code thingDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param featureDefinition the FeatureDefinition to retrieve the WoT TM from + * @param feature the feature 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeature(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + Feature feature, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code feature} of a Thing against the provided {@code thingModel}. + * + * @param thingDefinition the ThingDefinition which was used to retrieve the passed {@code thingModel} + * @param thingModel the ThingModel to validate against + * @param featureDefinition the FeatureDefinition which was used to retrieve the passed {@code featureThingModel} + * @param featureThingModel the feature ThingModel to validate against + * @param feature the feature 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeature(@Nullable ThingDefinition thingDefinition, + @Nullable ThingModel thingModel, + @Nullable FeatureDefinition featureDefinition, + @Nullable ThingModel featureThingModel, + Feature feature, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code feature} in context of a modification of its {@code featureDefinition} + * (e.g. in order to update it to a new version). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param featureDefinition the new, updated FeatureDefinition to retrieve the WoT TM from to validate against + * @param feature the Feature 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureDefinitionModification(@Nullable ThingDefinition thingDefinition, + FeatureDefinition featureDefinition, + Feature feature, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates if the deletion of a Feature's {@code featureDefinition} is allowed to do. + * Does so by just checking the configuration. + * + * @param thingDefinition the ThingDefinition of the Thing in which the FeatureDefinition should be deleted + * @param featureDefinition the FeatureDefinition to delete + * @param featureId the ID of the feature to validate the deletion in + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureDefinitionDeletion(@Nullable ThingDefinition thingDefinition, + FeatureDefinition featureDefinition, + String featureId, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code featureProperties} against the provided {@code featureDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param featureDefinition the FeatureDefinition to retrieve the WoT TM from + * @param featureId the ID of the feature to validate the properties for + * @param featureProperties the properties to validate + * @param desiredProperties whether the desired properties should be validated + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureProperties(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + String featureId, + @Nullable FeatureProperties featureProperties, + boolean desiredProperties, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code featureProperties} against the provided {@code featureDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param featureDefinition the FeatureDefinition which was used to retrieve the passed {@code featureThingModel} + * @param featureThingModel the feature's ThingModel to validate against + * @param featureId the ID of the feature to validate the properties for + * @param featureProperties the properties to validate + * @param desiredProperties whether the desired properties should be validated + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureProperties(@Nullable ThingDefinition thingDefinition, + FeatureDefinition featureDefinition, + ThingModel featureThingModel, + String featureId, + @Nullable FeatureProperties featureProperties, + boolean desiredProperties, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided feature property against the provided {@code featureDefinition} + * (if this links to a WoT TM). + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @param featureDefinition the FeatureDefinition to retrieve the WoT TM from + * @param featureId the ID of the feature to validate the properties for + * @param propertyPointer the pointer (path) of the property to validate + * @param propertyValue the property value to validate + * @param desiredProperty whether a desired property should be validated + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureProperty(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + String featureId, + JsonPointer propertyPointer, + JsonValue propertyValue, + boolean desiredProperty, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates a deletion inside a thing on "feature" scope, e.g. all feature properties or a single feature property. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM for the thing from + * @param featureDefinition the FeatureDefinition to retrieve the WoT TM for the feature from + * @param featureId the ID of the feature to validate the deletion for + * @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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureScopedDeletion(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + String featureId, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code inputPayload} of the feature message (WoT action) with subject + * {@code messageSubject} against the provided {@code featureDefinition}. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @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 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureActionInput(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + String featureId, + String messageSubject, + @Nullable JsonValue inputPayload, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code outputPayload} of the feature message (WoT action) with subject + * {@code messageSubject} against the provided {@code featureDefinition}. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @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 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureActionOutput(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + String featureId, + String messageSubject, + @Nullable JsonValue outputPayload, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Validates the provided {@code dataPayload} of the feature outbox message (WoT event) with subject + * {@code messageSubject} against the provided {@code featureDefinition}. + * + * @param thingDefinition the ThingDefinition to retrieve the WoT TM from + * @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 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 + * validation error - exceptionally finished with a {@link org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureEventData(@Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition, + String featureId, + String messageSubject, + @Nullable JsonValue dataPayload, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * Creates a new instance of WotThingModelValidator with the given {@code wotConfig}. + * + * @param wotConfig the WoT config to use. + * @param thingModelResolver the ThingModel resolver to fetch and resolve (extensions, refs) of linked other + * ThingModels during the generation process. + * @param executor the executor to use to run async tasks. + * @return the created WotThingModelValidator. + */ + static WotThingModelValidator of(final WotConfig wotConfig, + final WotThingModelResolver thingModelResolver, + final Executor executor + ) { + return new DefaultWotThingModelValidator(wotConfig, thingModelResolver, executor); + } +} diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/package-info.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/package-info.java new file mode 100644 index 0000000000..236a0d49dc --- /dev/null +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 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 + */ + +/** + * @since 3.6.0 + */ +@org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault +package org.eclipse.ditto.wot.api.validator; \ No newline at end of file diff --git a/wot/integration/pom.xml b/wot/integration/pom.xml index d1552409c0..44146afb07 100755 --- a/wot/integration/pom.xml +++ b/wot/integration/pom.xml @@ -34,10 +34,18 @@ org.eclipse.ditto ditto-base-model
+ + org.eclipse.ditto + ditto-wot-api + org.eclipse.ditto ditto-wot-model + + org.eclipse.ditto + ditto-wot-validation + org.eclipse.ditto ditto-things-model @@ -51,12 +59,6 @@ org.eclipse.ditto ditto-internal-utils-http - - - - - - diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/DefaultDittoWotIntegration.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/DefaultDittoWotIntegration.java new file mode 100644 index 0000000000..1230eeac3a --- /dev/null +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/DefaultDittoWotIntegration.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2024 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.wot.integration; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.util.concurrent.Executor; + +import javax.annotation.concurrent.Immutable; + +import org.apache.pekko.actor.AbstractExtensionId; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.actor.ExtendedActorSystem; +import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; +import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; +import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; +import org.eclipse.ditto.wot.api.config.DefaultWotConfig; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator; +import org.eclipse.ditto.wot.api.generator.WotThingModelExtensionResolver; +import org.eclipse.ditto.wot.api.generator.WotThingSkeletonGenerator; +import org.eclipse.ditto.wot.api.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; +import org.eclipse.ditto.wot.api.validator.WotThingModelValidator; + +/** + * Default Ditto specific implementation of {@link DittoWotIntegration} and Pekko extension. + */ +@Immutable +final class DefaultDittoWotIntegration implements DittoWotIntegration { + + private static final ThreadSafeDittoLogger LOGGER = + DittoLoggerFactory.getThreadSafeLogger(DefaultDittoWotIntegration.class); + + private final WotConfig wotConfig; + private final WotThingModelFetcher thingModelFetcher; + private final WotThingModelExtensionResolver thingModelExtensionResolver; + private final WotThingModelResolver thingModelResolver; + private final WotThingDescriptionGenerator thingDescriptionGenerator; + private final WotThingSkeletonGenerator thingSkeletonGenerator; + private final WotThingModelValidator thingModelValidator; + + private DefaultDittoWotIntegration(final ActorSystem actorSystem, final WotConfig wotConfig) { + this.wotConfig = checkNotNull(wotConfig, "wotConfig"); + LOGGER.info("Initializing DefaultDittoWotIntegration with config: {}", wotConfig); + + final Executor executor = actorSystem.dispatchers().lookup("wot-dispatcher"); + final Executor cacheLoaderExecutor = actorSystem.dispatchers().lookup("wot-dispatcher-cache-loader"); + final PekkoHttpJsonDownloader httpThingModelDownloader = + new PekkoHttpJsonDownloader(actorSystem, wotConfig); + thingModelFetcher = WotThingModelFetcher.of(wotConfig, httpThingModelDownloader, cacheLoaderExecutor); + thingModelExtensionResolver = WotThingModelExtensionResolver.of(thingModelFetcher, executor); + thingModelResolver = + WotThingModelResolver.of(wotConfig, thingModelFetcher, thingModelExtensionResolver, cacheLoaderExecutor); + thingDescriptionGenerator = WotThingDescriptionGenerator.of(wotConfig, thingModelResolver, executor); + thingSkeletonGenerator = WotThingSkeletonGenerator.of(wotConfig, thingModelResolver, executor); + thingModelValidator = WotThingModelValidator.of(wotConfig, thingModelResolver, executor); + } + + /** + * Returns a new {@code DefaultWotThingDescriptionProvider} for the given parameters. + * + * @param actorSystem the actor system to use. + * @param wotConfig the WoT config to use. + * @return the DefaultWotThingDescriptionProvider. + * @throws NullPointerException if any argument is {@code null}. + */ + public static DefaultDittoWotIntegration of(final ActorSystem actorSystem, final WotConfig wotConfig) { + return new DefaultDittoWotIntegration(actorSystem, wotConfig); + } + + @Override + public WotConfig getWotConfig() { + return wotConfig; + } + + @Override + public WotThingModelFetcher getWotThingModelFetcher() { + return thingModelFetcher; + } + + @Override + public WotThingModelResolver getWotThingModelResolver() { + return thingModelResolver; + } + + @Override + public WotThingDescriptionGenerator getWotThingDescriptionGenerator() { + return thingDescriptionGenerator; + } + + @Override + public WotThingModelExtensionResolver getWotThingModelExtensionResolver() { + return thingModelExtensionResolver; + } + + @Override + public WotThingSkeletonGenerator getWotThingSkeletonGenerator() { + return thingSkeletonGenerator; + } + + @Override + public WotThingModelValidator getWotThingModelValidator() { + return thingModelValidator; + } + + + static final class ExtensionId extends AbstractExtensionId { + + static final ExtensionId INSTANCE = new ExtensionId(); + + private ExtensionId() {} + + @Override + public DittoWotIntegration createExtension(final ExtendedActorSystem system) { + final WotConfig wotConfig = DefaultWotConfig.of( + DefaultScopedConfig.dittoScoped(system.settings().config()) + .getConfig(DefaultWotConfig.WOT_PARENT_CONFIG_PATH) + ); + return of(system, wotConfig); + } + } + +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/DittoWotIntegration.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/DittoWotIntegration.java new file mode 100644 index 0000000000..dc912b9214 --- /dev/null +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/DittoWotIntegration.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 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.wot.integration; + +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.actor.Extension; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.generator.WotThingDescriptionGenerator; +import org.eclipse.ditto.wot.api.generator.WotThingModelExtensionResolver; +import org.eclipse.ditto.wot.api.generator.WotThingSkeletonGenerator; +import org.eclipse.ditto.wot.api.provider.WotThingModelFetcher; +import org.eclipse.ditto.wot.api.resolver.WotThingModelResolver; +import org.eclipse.ditto.wot.api.validator.WotThingModelValidator; + +/** + * Extension providing access to all Ditto WoT integration capabilities. + * + * @since 3.6.0 + */ +public interface DittoWotIntegration extends Extension { + + /** + * @return the applied WoT configuration. + */ + WotConfig getWotConfig(); + + /** + * @return the WoT ThingModel fetcher used to download/fetch TMs from URLs. + */ + WotThingModelFetcher getWotThingModelFetcher(); + + /** + * @return the WoT ThingModel resolver which fetches ThingModels and in addition resolves extensions + * and reference to other ThingModels. + */ + WotThingModelResolver getWotThingModelResolver(); + + /** + * @return the WoT ThingDescription generator which generates ThingDescriptions based on ThingModels. + */ + WotThingDescriptionGenerator getWotThingDescriptionGenerator(); + + /** + * @return the WoT ThingModel extension and reference resolver used to resolve {@code tm:extends} and + * {@code tm:ref} constructs in ThingModels. + */ + WotThingModelExtensionResolver getWotThingModelExtensionResolver(); + + /** + * @return the WoT Thing skeleton generator which generates a JSON skeleton when creating new things + * or features based on a ThingModel, adhering to default values of the model. + */ + WotThingSkeletonGenerator getWotThingSkeletonGenerator(); + + /** + * @return the WoT ThingModel validator which can validate and enforce Ditto Thing payloads based on the + * defined ThingModel and its JsonSchema for properties, actions, events. + */ + WotThingModelValidator getWotThingModelValidator(); + + /** + * Get the {@code DittoWotIntegration} for an actor system. + * + * @param system the actor system. + * @return the {@code DittoWotIntegration} extension. + */ + static DittoWotIntegration get(final ActorSystem system) { + return DefaultDittoWotIntegration.ExtensionId.INSTANCE.get(system); + } +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/DefaultWotThingModelFetcher.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/PekkoHttpJsonDownloader.java similarity index 56% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/DefaultWotThingModelFetcher.java rename to wot/integration/src/main/java/org/eclipse/ditto/wot/integration/PekkoHttpJsonDownloader.java index 765897006a..342d184ce9 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/DefaultWotThingModelFetcher.java +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/PekkoHttpJsonDownloader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,39 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.provider; +package org.eclipse.ditto.wot.integration; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; -import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - -import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; -import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; -import org.eclipse.ditto.internal.utils.cache.Cache; -import org.eclipse.ditto.internal.utils.cache.CacheFactory; -import org.eclipse.ditto.internal.utils.http.DefaultHttpClientFacade; -import org.eclipse.ditto.internal.utils.http.HttpClientFacade; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonValue; -import org.eclipse.ditto.wot.integration.config.WotConfig; -import org.eclipse.ditto.wot.model.IRI; -import org.eclipse.ditto.wot.model.ThingDefinitionInvalidException; -import org.eclipse.ditto.wot.model.ThingModel; -import org.eclipse.ditto.wot.model.WotThingModelInvalidException; -import org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException; - -import com.github.benmanes.caffeine.cache.AsyncCacheLoader; import org.apache.pekko.actor.ActorSystem; import org.apache.pekko.http.javadsl.model.HttpHeader; @@ -56,16 +32,25 @@ import org.apache.pekko.stream.SystemMaterializer; import org.apache.pekko.stream.javadsl.Sink; import org.apache.pekko.util.ByteString; +import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; +import org.eclipse.ditto.internal.utils.http.DefaultHttpClientFacade; +import org.eclipse.ditto.internal.utils.http.HttpClientFacade; +import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; +import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.wot.api.config.WotConfig; +import org.eclipse.ditto.wot.api.provider.JsonDownloader; +import org.eclipse.ditto.wot.model.WotThingModelNotAccessibleException; /** - * Default implementation of {@link WotThingModelFetcher} which should be not Ditto specific. + * Pekko HTTP based implementation of {@link JsonDownloader}. */ -final class DefaultWotThingModelFetcher implements WotThingModelFetcher { +final class PekkoHttpJsonDownloader implements JsonDownloader { private static final ThreadSafeDittoLogger LOGGER = - DittoLoggerFactory.getThreadSafeLogger(DefaultWotThingModelFetcher.class); - - private static final Duration MAX_FETCH_MODEL_DURATION = Duration.ofSeconds(10); + DittoLoggerFactory.getThreadSafeLogger(PekkoHttpJsonDownloader.class); private static final HttpHeader ACCEPT_HEADER = Accept.create( MediaRanges.create(MediaTypes.applicationWithOpenCharset("tm+json")), @@ -74,61 +59,21 @@ final class DefaultWotThingModelFetcher implements WotThingModelFetcher { private final HttpClientFacade httpClient; private final Materializer materializer; - private final Cache thingModelCache; - DefaultWotThingModelFetcher(final ActorSystem actorSystem, final WotConfig wotConfig) { + PekkoHttpJsonDownloader(final ActorSystem actorSystem, final WotConfig wotConfig) { this.httpClient = DefaultHttpClientFacade.getInstance(actorSystem, wotConfig.getHttpProxyConfig()); materializer = SystemMaterializer.get(actorSystem).materializer(); - final AsyncCacheLoader loader = this::loadThingModelViaHttp; - thingModelCache = CacheFactory.createCache(loader, - wotConfig.getCacheConfig(), - "ditto_wot_thing_model_cache", - actorSystem.dispatchers().lookup("wot-dispatcher-cache-loader")); - } - - @Override - public CompletableFuture fetchThingModel(final IRI iri, final DittoHeaders dittoHeaders) { - try { - return fetchThingModel(new URL(iri.toString()), dittoHeaders); - } catch (final MalformedURLException e) { - throw ThingDefinitionInvalidException.newBuilder(iri) - .dittoHeaders(dittoHeaders) - .build(); - } } @Override - public CompletableFuture fetchThingModel(final URL url, final DittoHeaders dittoHeaders) { - LOGGER.withCorrelationId(dittoHeaders) - .debug("Fetching ThingModel (from cache or downloading as fallback) from URL: <{}>", url); - return thingModelCache.get(url) - .thenApply(optTm -> resolveThingModel(optTm.orElse(null), url, dittoHeaders)) - .orTimeout(MAX_FETCH_MODEL_DURATION.toSeconds(), TimeUnit.SECONDS); - } - - private ThingModel resolveThingModel(@Nullable final ThingModel thingModel, - final URL tmUrl, - final DittoHeaders dittoHeaders) { - if (null != thingModel) { - LOGGER.withCorrelationId(dittoHeaders).debug("Resolved ThingModel: <{}>", thingModel); - return thingModel; - } else { - throw WotThingModelNotAccessibleException.newBuilder(tmUrl) - .dittoHeaders(dittoHeaders) - .build(); - } - } - - /* this method is used to asynchronously load the ThingModel into the cache */ - private CompletableFuture loadThingModelViaHttp(final URL url, final Executor executor) { - LOGGER.debug("Loading ThingModel from URL <{}>.", url); - final CompletionStage responseFuture = getThingModelFromUrl(url); - final CompletionStage thingModelFuture = responseFuture.thenCompose( - response -> mapResponseToThingModel(response, url)); + public CompletionStage downloadJsonViaHttp(final URL url, final Executor executor) { + LOGGER.debug("Loading JsonObject from URL <{}>.", url); + final CompletionStage responseFuture = getJsonObjectFromUrl(url); + final CompletionStage thingModelFuture = responseFuture.thenCompose(this::mapResponseToJsonObject); return thingModelFuture.toCompletableFuture(); } - private CompletionStage getThingModelFromUrl(final URL url) { + private CompletionStage getJsonObjectFromUrl(final URL url) { return httpClient.createSingleHttpRequest(HttpRequest.GET(url.toString()).withHeaders(List.of(ACCEPT_HEADER))) .thenCompose(response -> { if (response.status().isRedirection()) { @@ -142,7 +87,7 @@ private CompletionStage getThingModelFromUrl(final URL url) { cause -> handleUnexpectedException(cause, url)); } }) - .map(this::getThingModelFromUrl) // recurse following the redirect + .map(this::getJsonObjectFromUrl) // recurse following the redirect .orElseGet(() -> CompletableFuture.completedFuture(response)); } else { return CompletableFuture.completedFuture(response); @@ -160,20 +105,6 @@ private CompletionStage getThingModelFromUrl(final URL url) { }); } - private CompletableFuture mapResponseToThingModel(final HttpResponse response, final URL url) { - final CompletableFuture bodyFuture = mapResponseToJsonObject(response) - .toCompletableFuture(); - return bodyFuture - .thenApply(ThingModel::fromJson) - .exceptionally(t -> { - LOGGER.warn("Failed to extract ThingModel from response <{}> because of <{}: {}>", response, - t.getClass().getSimpleName(), t.getMessage()); - throw WotThingModelInvalidException.newBuilder(url) - .cause(t) - .build(); - }); - } - private CompletionStage mapResponseToJsonObject(final HttpResponse response) { return response.entity().getDataBytes().fold(ByteString.emptyByteString(), ByteString::concat) .map(ByteString::utf8String) @@ -184,7 +115,7 @@ private CompletionStage mapResponseToJsonObject(final HttpResponse r private void handleNonSuccessResponse(final HttpResponse response, final URL url) { final String msg = MessageFormat.format( - "Got non success response from ThingModel endpoint with status code: <{0}>", response.status()); + "Got non success response from JsonObject endpoint with status code: <{0}>", response.status()); getBodyAsString(response) .thenAccept(stringBody -> LOGGER.info("{} and body: <{}>.", msg, stringBody)); throw WotThingModelNotAccessibleException.newBuilder(url) @@ -199,7 +130,7 @@ private CompletionStage getBodyAsString(final HttpResponse response) { private static DittoRuntimeException handleUnexpectedException(final Throwable e, final URL url) { - final String msg = MessageFormat.format("Got Exception from ThingModel endpoint <{0}>.", url); + final String msg = MessageFormat.format("Got Exception from JsonObject endpoint <{0}>.", url); LOGGER.warn(msg, e); throw WotThingModelNotAccessibleException.newBuilder(url) .cause(e) diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingDescriptionGenerator.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingDescriptionGenerator.java deleted file mode 100644 index b3eb6ad9eb..0000000000 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingDescriptionGenerator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2022 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.wot.integration.generator; - -import java.net.URL; -import java.util.concurrent.CompletionStage; - -import javax.annotation.Nullable; - -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.things.model.Thing; -import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.wot.integration.config.WotConfig; -import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; -import org.eclipse.ditto.wot.model.ThingDescription; -import org.eclipse.ditto.wot.model.ThingModel; - -import org.apache.pekko.actor.ActorSystem; - -/** - * Generator for WoT (Web of Things) {@link ThingDescription} based on a given WoT {@link ThingModel} and context of the - * Ditto {@link Thing} to generate the ThingDescription for. - * - * @since 2.4.0 - */ -public interface WotThingDescriptionGenerator { - - /** - * Generates a ThingDescription for the given {@code thingId}, optionally using the passed {@code thing} to lookup - * thing specific placeholders. - * Uses the passed in {@code thingModel} and generates TD forms, security definition etc. in order to make it a - * valid TD. - * - * @param thingId the ThingId to generate the ThingDescription for. - * @param thing the optional Thing from which to resolve metadata from. - * @param placeholderLookupObject the optional JsonObject to dynamically resolve placeholders from - * (e.g. a Thing or Feature). - * @param featureId the optional feature name if the TD should be generated for a certain feature of the Thing. - * @param thingModel the ThingModel to use as template for generating the TD. - * @param thingModelUrl the URL from which the ThingModel was fetched. - * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeException which might occur during the - * generation. - * @return the generated ThingDescription for the given {@code thingId} based on the passed in {@code thingModel}. - * @throws org.eclipse.ditto.wot.model.WotThingModelInvalidException if the WoT ThingModel did not contain the - * mandatory {@code "@type"} being {@code "tm:ThingModel"} - */ - CompletionStage generateThingDescription(ThingId thingId, - @Nullable Thing thing, - @Nullable JsonObject placeholderLookupObject, - @Nullable String featureId, - ThingModel thingModel, - URL thingModelUrl, - DittoHeaders dittoHeaders); - - /** - * Creates a new instance of WotThingDescriptionGenerator with the given {@code wotConfig}. - * - * @param actorSystem the actor system to use. - * @param wotConfig the WoTConfig to use for creating the generator. - * @param thingModelFetcher the ThingModel fetcher to fetch linked other ThingModels during the TD generation - * process. - * @return the created WotThingDescriptionGenerator. - */ - static WotThingDescriptionGenerator of(final ActorSystem actorSystem, - final WotConfig wotConfig, - final WotThingModelFetcher thingModelFetcher) { - return new DefaultWotThingDescriptionGenerator(actorSystem, wotConfig, thingModelFetcher); - } -} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/package-info.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/package-info.java new file mode 100644 index 0000000000..c7e6892b61 --- /dev/null +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 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 + */ + +/** + * The Ditto WoT integration using the "Ditto WoT API" with Akka specifics, e.g. in order to fetch WoT models via HTTP. + * @since 3.6.0 + */ +@org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault +package org.eclipse.ditto.wot.integration; \ No newline at end of file diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/DefaultWotThingDescriptionProvider.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/DefaultWotThingDescriptionProvider.java deleted file mode 100644 index 4e51f11e45..0000000000 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/provider/DefaultWotThingDescriptionProvider.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (c) 2022 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.wot.integration.provider; - -import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; - -import java.net.URL; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Executor; - -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; - -import org.apache.pekko.actor.AbstractExtensionId; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.actor.ExtendedActorSystem; -import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.signals.FeatureToggle; -import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; -import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; -import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; -import org.eclipse.ditto.json.JsonValue; -import org.eclipse.ditto.things.model.DefinitionIdentifier; -import org.eclipse.ditto.things.model.Feature; -import org.eclipse.ditto.things.model.FeatureDefinition; -import org.eclipse.ditto.things.model.Thing; -import org.eclipse.ditto.things.model.ThingDefinition; -import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.wot.integration.config.DefaultWotConfig; -import org.eclipse.ditto.wot.integration.config.WotConfig; -import org.eclipse.ditto.wot.integration.generator.WotThingDescriptionGenerator; -import org.eclipse.ditto.wot.integration.generator.WotThingSkeletonGenerator; -import org.eclipse.ditto.wot.model.ThingDefinitionInvalidException; -import org.eclipse.ditto.wot.model.ThingDescription; -import org.eclipse.ditto.wot.model.WotInternalErrorException; - -/** - * Default Ditto specific implementation of {@link WotThingDescriptionProvider}. - */ -@Immutable -final class DefaultWotThingDescriptionProvider implements WotThingDescriptionProvider { - - private static final ThreadSafeDittoLogger LOGGER = - DittoLoggerFactory.getThreadSafeLogger(DefaultWotThingDescriptionProvider.class); - - public static final String MODEL_PLACEHOLDERS_KEY = "model-placeholders"; - - private final WotConfig wotConfig; - private final WotThingModelFetcher thingModelFetcher; - private final WotThingDescriptionGenerator thingDescriptionGenerator; - private final WotThingSkeletonGenerator thingSkeletonGenerator; - private final Executor executor; - - private DefaultWotThingDescriptionProvider(final ActorSystem actorSystem, final WotConfig wotConfig) { - this.wotConfig = checkNotNull(wotConfig, "wotConfig"); - thingModelFetcher = new DefaultWotThingModelFetcher(actorSystem, wotConfig); - thingDescriptionGenerator = WotThingDescriptionGenerator.of(actorSystem, wotConfig, thingModelFetcher); - thingSkeletonGenerator = WotThingSkeletonGenerator.of(actorSystem, thingModelFetcher); - executor = actorSystem.dispatchers().lookup("wot-dispatcher"); - } - - /** - * Returns a new {@code DefaultWotThingDescriptionProvider} for the given parameters. - * - * @param actorSystem the actor system to use. - * @param wotConfig the WoT config to use. - * @return the DefaultWotThingDescriptionProvider. - * @throws NullPointerException if any argument is {@code null}. - */ - public static DefaultWotThingDescriptionProvider of(final ActorSystem actorSystem, final WotConfig wotConfig) { - return new DefaultWotThingDescriptionProvider(actorSystem, wotConfig); - } - - @Override - public CompletionStage provideThingTD(@Nullable final ThingDefinition thingDefinition, - final ThingId thingId, - @Nullable final Thing thing, - final DittoHeaders dittoHeaders) { - if (null != thingDefinition) { - return getWotThingDescriptionForThing(thingDefinition, thingId, thing, dittoHeaders); - } else { - throw ThingDefinitionInvalidException.newBuilder(null) - .dittoHeaders(dittoHeaders) - .build(); - } - } - - @Override - public CompletionStage provideFeatureTD(final ThingId thingId, - @Nullable final Thing thing, - final Feature feature, - final DittoHeaders dittoHeaders) { - - checkNotNull(feature, "feature"); - if (feature.getDefinition().isPresent()) { - return getWotThingDescriptionForFeature(thingId, thing, feature, dittoHeaders); - } else { - throw ThingDefinitionInvalidException.newBuilder(null) - .dittoHeaders(dittoHeaders) - .build(); - } - } - - @Override - public CompletionStage> provideThingSkeletonForCreation(final ThingId thingId, - @Nullable final ThingDefinition thingDefinition, - final DittoHeaders dittoHeaders) { - - final ThreadSafeDittoLogger logger = LOGGER.withCorrelationId(dittoHeaders); - if (FeatureToggle.isWotIntegrationFeatureEnabled() && - wotConfig.getCreationConfig().getThingCreationConfig().isSkeletonCreationEnabled() && - null != thingDefinition) { - final Optional urlOpt = thingDefinition.getUrl(); - if (urlOpt.isPresent()) { - final URL url = urlOpt.get(); - logger.debug("Fetching ThingModel from <{}> in order to create Thing skeleton for new Thing " + - "with id <{}>", url, thingId); - return thingModelFetcher.fetchThingModel(url, dittoHeaders) - .thenComposeAsync(thingModel -> thingSkeletonGenerator.generateThingSkeleton( - thingId, - thingModel, - url, - wotConfig.getCreationConfig() - .getThingCreationConfig() - .shouldGenerateDefaultsForOptionalProperties(), - dittoHeaders - ), - executor - ) - .handle((thingSkeleton, throwable) -> { - if (throwable != null) { - logger.info("Could not fetch ThingModel or generate Thing skeleton based on it due " + - "to: <{}: {}>", - throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); - if (wotConfig.getCreationConfig() - .getThingCreationConfig() - .shouldThrowExceptionOnWotErrors()) { - throw DittoRuntimeException.asDittoRuntimeException( - throwable, t -> WotInternalErrorException.newBuilder() - .dittoHeaders(dittoHeaders) - .cause(t) - .build() - ); - } else { - return Optional.empty(); - } - } else { - logger.debug("Created Thing skeleton for new Thing with id <{}>: <{}>", thingId, - thingSkeleton); - return thingSkeleton; - } - }); - } else { - return CompletableFuture.completedFuture(Optional.empty()); - } - } else { - return CompletableFuture.completedFuture(Optional.empty()); - } - } - - @Override - public CompletionStage> provideFeatureSkeletonForCreation(final String featureId, - @Nullable final FeatureDefinition featureDefinition, final DittoHeaders dittoHeaders) { - - final ThreadSafeDittoLogger logger = LOGGER.withCorrelationId(dittoHeaders); - if (FeatureToggle.isWotIntegrationFeatureEnabled() && - wotConfig.getCreationConfig().getFeatureCreationConfig().isSkeletonCreationEnabled() && - null != featureDefinition) { - final Optional urlOpt = featureDefinition.getFirstIdentifier().getUrl(); - if (urlOpt.isPresent()) { - final URL url = urlOpt.get(); - logger.debug("Fetching ThingModel from <{}> in order to create Feature skeleton for new Feature " + - "with id <{}>", url, featureId); - return thingModelFetcher.fetchThingModel(url, dittoHeaders) - .thenComposeAsync(thingModel -> thingSkeletonGenerator.generateFeatureSkeleton( - featureId, - thingModel, - url, - wotConfig.getCreationConfig() - .getFeatureCreationConfig() - .shouldGenerateDefaultsForOptionalProperties(), - dittoHeaders - ), - executor - ) - .handle((featureSkeleton, throwable) -> { - if (throwable != null) { - logger.info("Could not fetch ThingModel or generate Feature skeleton based on it due " + - "to: <{}: {}>", - throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); - if (wotConfig.getCreationConfig() - .getFeatureCreationConfig() - .shouldThrowExceptionOnWotErrors()) { - throw DittoRuntimeException.asDittoRuntimeException( - throwable, t -> WotInternalErrorException.newBuilder() - .dittoHeaders(dittoHeaders) - .cause(t) - .build() - ); - } else { - return Optional.empty(); - } - } else { - logger.debug("Created Feature skeleton for new Feature with id <{}>: <{}>", featureId, - featureSkeleton); - return featureSkeleton; - } - }); - } else { - return CompletableFuture.completedFuture(Optional.empty()); - } - } else { - return CompletableFuture.completedFuture(Optional.empty()); - } - } - - /** - * Download TM, add it to local cache + build TD + return it - */ - private CompletionStage getWotThingDescriptionForThing(final ThingDefinition definitionIdentifier, - final ThingId thingId, - @Nullable final Thing thing, - final DittoHeaders dittoHeaders) { - - final Optional urlOpt = definitionIdentifier.getUrl(); - if (urlOpt.isPresent()) { - final URL url = urlOpt.get(); - return thingModelFetcher.fetchThingModel(url, dittoHeaders) - .thenComposeAsync(thingModel -> thingDescriptionGenerator - .generateThingDescription(thingId, - thing, - Optional.ofNullable(thing) - .flatMap(Thing::getAttributes) - .flatMap(a -> a.getValue(MODEL_PLACEHOLDERS_KEY)) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .orElse(null), - null, - thingModel, - url, - dittoHeaders - ), - executor - ) - .exceptionally(throwable -> { - throw DittoRuntimeException.asDittoRuntimeException(throwable, t -> - WotInternalErrorException.newBuilder() - .dittoHeaders(dittoHeaders) - .cause(t) - .build()); - }); - } else { - throw ThingDefinitionInvalidException.newBuilder(definitionIdentifier) - .dittoHeaders(dittoHeaders) - .build(); - } - } - - /** - * Download TM, add it to local cache + build TD + return it - */ - private CompletionStage getWotThingDescriptionForFeature(final ThingId thingId, - @Nullable final Thing thing, - final Feature feature, - final DittoHeaders dittoHeaders) { - - final Optional definitionIdentifier = feature.getDefinition() - .map(FeatureDefinition::getFirstIdentifier); - final Optional urlOpt = definitionIdentifier.flatMap(DefinitionIdentifier::getUrl); - if (urlOpt.isPresent()) { - final URL url = urlOpt.get(); - return thingModelFetcher.fetchThingModel(url, dittoHeaders) - .thenComposeAsync(thingModel -> thingDescriptionGenerator - .generateThingDescription(thingId, - thing, - feature.getProperties() - .flatMap(p -> p.getValue(MODEL_PLACEHOLDERS_KEY)) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .orElse(null), - feature.getId(), - thingModel, - url, - dittoHeaders - ), - executor - ) - .exceptionally(throwable -> { - throw DittoRuntimeException.asDittoRuntimeException(throwable, t -> - WotInternalErrorException.newBuilder() - .dittoHeaders(dittoHeaders) - .cause(t) - .build()); - }); - } else { - throw ThingDefinitionInvalidException.newBuilder(definitionIdentifier.orElse(null)) - .dittoHeaders(dittoHeaders) - .build(); - } - } - - static final class ExtensionId extends AbstractExtensionId { - - private static final String WOT_PARENT_CONFIG_PATH = "things"; - - static final ExtensionId INSTANCE = new ExtensionId(); - - private ExtensionId() {} - - @Override - public WotThingDescriptionProvider createExtension(final ExtendedActorSystem system) { - final WotConfig wotConfig = DefaultWotConfig.of( - DefaultScopedConfig.dittoScoped(system.settings().config()).getConfig(WOT_PARENT_CONFIG_PATH) - ); - return of(system, wotConfig); - } - } - -} diff --git a/wot/model/pom.xml b/wot/model/pom.xml index 9ea16a3627..6c3f4fe0ff 100755 --- a/wot/model/pom.xml +++ b/wot/model/pom.xml @@ -91,6 +91,7 @@ + org.atteo.classindex, !org.eclipse.ditto.utils.jsr305.annotations, org.eclipse.ditto.* diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java index d256f7ca6c..a4afdb9433 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java @@ -16,6 +16,7 @@ import java.util.Collection; import java.util.Optional; +import java.util.function.Consumer; import javax.annotation.Nullable; @@ -184,6 +185,12 @@ public B setType(@Nullable final DataSchemaType type) { return myself; } + @Override + public B enhanceObjectBuilder(final Consumer builderConsumer) { + builderConsumer.accept(wrappedObjectBuilder); + return myself; + } + protected void putValue(final JsonFieldDefinition definition, @Nullable final J value) { final Optional keyOpt = definition.getPointer().getRoot(); if (keyOpt.isPresent()) { diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DittoWotExtension.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/DittoWotExtension.java similarity index 74% rename from wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DittoWotExtension.java rename to wot/model/src/main/java/org/eclipse/ditto/wot/model/DittoWotExtension.java index 26453f567b..0eead261e1 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DittoWotExtension.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/DittoWotExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -10,9 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.integration.generator; - -import org.eclipse.ditto.wot.model.SingleUriAtContext; +package org.eclipse.ditto.wot.model; /** * Contains the specifics of Ditto's WoT Extension Ontology. @@ -20,12 +18,12 @@ * @see Ditto - WoT Extension Ontology * @since 3.0.0 */ -final class DittoWotExtension { +public final class DittoWotExtension { /** * The {@code SingleUriAtContext} (being an IRI) of the Ditto WoT Extension. */ - static final SingleUriAtContext DITTO_WOT_EXTENSION = SingleUriAtContext.DITTO_WOT_EXTENSION; + public static final SingleUriAtContext DITTO_WOT_EXTENSION = SingleUriAtContext.DITTO_WOT_EXTENSION; /** * Contains a category with which WoT property affordances may optionally be categorized. @@ -33,7 +31,7 @@ final class DittoWotExtension { * * @see Property category */ - static final String DITTO_WOT_EXTENSION_CATEGORY = "category"; + public static final String DITTO_WOT_EXTENSION_CATEGORY = "category"; private DittoWotExtension() { diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/ImmutableDataSchemaWithoutType.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/ImmutableDataSchemaWithoutType.java new file mode 100644 index 0000000000..b42a1ffed0 --- /dev/null +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/ImmutableDataSchemaWithoutType.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 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.wot.model; + +import java.util.Optional; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.json.JsonObject; + +/** + * Immutable implementation of {@link org.eclipse.ditto.wot.model.SingleDataSchema} without a known {@code type}. + */ +@Immutable +final class ImmutableDataSchemaWithoutType extends AbstractSingleDataSchema { + + ImmutableDataSchemaWithoutType(final JsonObject wrappedObject) { + super(wrappedObject); + } + + @Override + public Optional getType() { + return Optional.empty(); + } + + @Override + protected boolean canEqual(@Nullable final Object other) { + return other instanceof ImmutableDataSchemaWithoutType; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + super.toString() + "]"; + } +} diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java index 68b313b794..fb3dea12c0 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java @@ -13,6 +13,7 @@ package org.eclipse.ditto.wot.model; import java.util.Collection; +import java.util.function.Consumer; import javax.annotation.Nullable; @@ -127,6 +128,12 @@ public Property.Builder setDefault(@Nullable final JsonValue defaultValue) { return myself; } + @Override + public Property.Builder enhanceObjectBuilder(final Consumer builderConsumer) { + builderConsumer.accept(wrappedObjectBuilder); + return myself; + } + @Override public Property.Builder setType(@Nullable final DataSchemaType type) { if (type != null) { diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java index 036a69c8ae..8de9c2145f 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -25,6 +26,7 @@ import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonFieldDefinition; import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; import org.eclipse.ditto.json.JsonValue; /** @@ -68,8 +70,7 @@ static SingleDataSchema fromJson(final JsonObject jsonObject) { throw new IllegalArgumentException("Unsupported dataSchema-type: " + type); } }) - .orElseThrow(() -> new IllegalArgumentException("Could not create SingleDataSchema - " + - "json field <" + DataSchemaJsonFields.TYPE.getPointer() + "> was missing or unknown")); + .orElseGet(() -> new ImmutableDataSchemaWithoutType(jsonObject)); } static BooleanSchema.Builder newBooleanSchemaBuilder() { @@ -158,6 +159,8 @@ interface Builder, S extends SingleDataSchema> { B setDefault(@Nullable JsonValue defaultValue); + B enhanceObjectBuilder(Consumer builderConsumer); + S build(); } diff --git a/wot/pom.xml b/wot/pom.xml index 4847a05617..93d142b8b7 100644 --- a/wot/pom.xml +++ b/wot/pom.xml @@ -1,6 +1,6 @@ + + 4.0.0 + + + org.eclipse.ditto + ditto-wot + ${revision} + + + ditto-wot-validation + Eclipse Ditto :: WoT :: Validation + + + + + + + org.eclipse.ditto + ditto-json + + + org.eclipse.ditto + ditto-base-model + + + org.eclipse.ditto + ditto-things-model + + + + org.eclipse.ditto + ditto-wot-model + + + + org.eclipse.ditto + ditto-internal-utils-config + + + org.eclipse.ditto + ditto-internal-utils-json + + + + com.networknt + json-schema-validator + + + + ch.qos.logback + logback-classic + test + + + + \ No newline at end of file diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/DefaultWotThingModelValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/DefaultWotThingModelValidation.java new file mode 100644 index 0000000000..4bcaf1d748 --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/DefaultWotThingModelValidation.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforceFeatureActionPayload; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforceFeatureEventPayload; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforceFeatureProperties; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforceFeaturePropertiesInAllSubmodels; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforceFeatureProperty; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforcePresenceOfModeledFeatures; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.enforcePresenceOfRequiredPropertiesUponFeatureLevelDeletion; +import static org.eclipse.ditto.wot.validation.InternalFeatureValidation.forbidNonModeledFeatures; +import static org.eclipse.ditto.wot.validation.InternalThingValidation.enforcePresenceOfRequiredPropertiesUponThingLevelDeletion; +import static org.eclipse.ditto.wot.validation.InternalThingValidation.enforceThingActionPayload; +import static org.eclipse.ditto.wot.validation.InternalThingValidation.enforceThingAttribute; +import static org.eclipse.ditto.wot.validation.InternalThingValidation.enforceThingAttributes; +import static org.eclipse.ditto.wot.validation.InternalThingValidation.enforceThingEventPayload; +import static org.eclipse.ditto.wot.validation.InternalValidation.success; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.json.JsonKey; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; + +/** + * Default implementation for WoT ThingModel based validation/enforcement. + */ +final class DefaultWotThingModelValidation implements WotThingModelValidation { + + private final TmValidationConfig validationConfig; + + DefaultWotThingModelValidation(final TmValidationConfig validationConfig) { + this.validationConfig = validationConfig; + } + + @Override + public CompletionStage validateThingAttributes(final ThingModel thingModel, + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getThingValidationConfig().isEnforceAttributes() && attributes != null) { + return enforceThingAttributes(thingModel, + attributes, + validationConfig.getThingValidationConfig().isForbidNonModeledAttributes(), + resourcePath, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateThingAttribute(final ThingModel thingModel, + final JsonPointer attributePointer, + final JsonValue attributeValue, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getThingValidationConfig().isEnforceAttributes()) { + return enforceThingAttribute(thingModel, + attributePointer, + attributeValue, + validationConfig.getThingValidationConfig().isForbidNonModeledAttributes(), + resourcePath, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateThingScopedDeletion(final ThingModel thingModel, + final Map featureThingModels, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (resourcePath.getRoot().filter(DefaultWotThingModelValidation::isConcerningAttributes).isPresent() && + validationConfig.getThingValidationConfig().isEnforceAttributes() + ) { + return enforcePresenceOfRequiredPropertiesUponThingLevelDeletion(thingModel, + resourcePath, + context + ); + } + + if (resourcePath.getRoot().filter(DefaultWotThingModelValidation::isConcerningFeatures).isPresent() && + validationConfig.getFeatureValidationConfig().isEnforcePresenceOfModeledFeatures() && + resourcePath.getLevelCount() == 1 && !featureThingModels.isEmpty() + ) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not delete all Features, " + + "as there are submodels defined as in the Thing's model"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + // all other cases should not be handled here, but in "validateFeatureScopedDeletion" + + return success(); + } + + private static boolean isConcerningAttributes(final JsonKey root) { + return Thing.JsonFields.ATTRIBUTES.getPointer().getRoot().orElseThrow().equals(root); + } + + private static boolean isConcerningFeatures(final JsonKey root) { + return Thing.JsonFields.FEATURES.getPointer().getRoot().orElseThrow().equals(root); + } + + @Override + public CompletionStage validateThingActionInput(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getThingValidationConfig().isEnforceInboxMessagesInput()) { + return enforceThingActionPayload(thingModel, + messageSubject, + inputPayload, + validationConfig.getThingValidationConfig().isForbidNonModeledInboxMessages(), + resourcePath, + true, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateThingActionOutput(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue outputPayload, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getThingValidationConfig().isEnforceInboxMessagesOutput()) { + return enforceThingActionPayload(thingModel, + messageSubject, + outputPayload, + validationConfig.getThingValidationConfig().isForbidNonModeledInboxMessages(), + resourcePath, + false, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateThingEventData(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue dataPayload, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getThingValidationConfig().isEnforceOutboxMessages()) { + return enforceThingEventPayload(thingModel, + messageSubject, + dataPayload, + validationConfig.getThingValidationConfig().isForbidNonModeledOutboxMessages(), + resourcePath, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateFeaturesPresence(final Map featureThingModels, + @Nullable final Features features, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (validationConfig.getFeatureValidationConfig().isEnforcePresenceOfModeledFeatures()) { + firstStage = enforcePresenceOfModeledFeatures(features, featureThingModels.keySet(), context); + } else { + firstStage = success(); + } + + final CompletableFuture secondStage; + if (validationConfig.getFeatureValidationConfig().isForbidNonModeledFeatures()) { + secondStage = forbidNonModeledFeatures(features, featureThingModels.keySet(), context); + } else { + secondStage = success(); + } + return firstStage.thenCompose(unused -> secondStage); + } + + @Override + public CompletionStage validateFeaturesProperties(final Map featureThingModels, + @Nullable final Features features, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (validationConfig.getFeatureValidationConfig().isEnforceProperties() && features != null) { + firstStage = enforceFeaturePropertiesInAllSubmodels( + featureThingModels, + features, + false, + validationConfig.getFeatureValidationConfig().isForbidNonModeledProperties(), + resourcePath, + context + ).thenApply(aVoid -> null); + } else { + firstStage = success(); + } + + final CompletableFuture secondStage; + if (validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties() && features != null) { + secondStage = enforceFeaturePropertiesInAllSubmodels( + featureThingModels, + features, + true, + validationConfig.getFeatureValidationConfig().isForbidNonModeledDesiredProperties(), + resourcePath, + context + ).thenApply(aVoid -> null); + } else { + secondStage = success(); + } + return firstStage.thenCompose(unused -> secondStage); + } + + + @Override + public CompletionStage validateFeaturePresence(final Map featureThingModels, + final Feature feature, + final ValidationContext context + ) { + final Set definedFeatureIds = featureThingModels.keySet(); + final String featureId = feature.getId(); + if (validationConfig.getFeatureValidationConfig().isForbidNonModeledFeatures() && + !definedFeatureIds.contains(featureId)) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Attempting to update the Thing with a feature which is not " + + "defined in the model: <" + featureId + ">"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + @Override + public CompletionStage validateFeature(final ThingModel featureThingModel, + final Feature feature, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (validationConfig.getFeatureValidationConfig().isEnforceProperties()) { + firstStage = enforceFeatureProperties(featureThingModel, + feature, + false, + validationConfig.getFeatureValidationConfig().isForbidNonModeledProperties(), + resourcePath, + context + ); + } else { + firstStage = success(); + } + + final CompletableFuture secondStage; + if (validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties()) { + secondStage = enforceFeatureProperties(featureThingModel, + feature, + true, + validationConfig.getFeatureValidationConfig().isForbidNonModeledDesiredProperties(), + resourcePath, + context + ); + } else { + secondStage = success(); + } + return firstStage.thenCompose(unused -> secondStage); + } + + @Override + public CompletionStage validateFeatureProperties(final ThingModel featureThingModel, + final String featureId, + @Nullable final FeatureProperties featureProperties, + final boolean desiredProperties, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if ( + (!desiredProperties && + validationConfig.getFeatureValidationConfig().isEnforceProperties()) || + (desiredProperties && + validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties()) + ) { + return enforceFeatureProperties(featureThingModel, + desiredProperties ? + Feature.newBuilder().desiredProperties(featureProperties).withId(featureId).build() : + Feature.newBuilder().properties(featureProperties).withId(featureId).build(), + desiredProperties, + desiredProperties ? + validationConfig.getFeatureValidationConfig().isForbidNonModeledDesiredProperties() : + validationConfig.getFeatureValidationConfig().isForbidNonModeledProperties(), + resourcePath, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateFeatureProperty(final ThingModel featureThingModel, + final String featureId, + final JsonPointer propertyPointer, + final JsonValue propertyValue, + final boolean desiredProperty, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getFeatureValidationConfig().isEnforceProperties()) { + return enforceFeatureProperty(featureThingModel, + featureId, + propertyPointer, + propertyValue, + desiredProperty, + desiredProperty ? + validationConfig.getFeatureValidationConfig().isForbidNonModeledDesiredProperties() : + validationConfig.getFeatureValidationConfig().isForbidNonModeledProperties(), + resourcePath, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateFeatureScopedDeletion(final Map featureThingModels, + final ThingModel featureThingModel, + final String featureId, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getFeatureValidationConfig().isEnforcePresenceOfModeledFeatures() && + resourcePath.equals(Thing.JsonFields.FEATURES.getPointer().addLeaf(JsonKey.of(featureId))) && + featureThingModels.containsKey(featureId) + ) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not delete Feature <" + featureId + ">, " + + "as it is defined as submodel in the Thing's model"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + + if (validationConfig.getFeatureValidationConfig().isEnforceProperties()) { + return enforcePresenceOfRequiredPropertiesUponFeatureLevelDeletion(featureThingModel, + featureId, + resourcePath, + context + ); + } + // desired properties do not need to be checked, as the "required" definitions do not apply for them + // all desired properties are optional by default + return success(); + } + + @Override + public CompletionStage validateFeatureActionInput(final ThingModel featureThingModel, + final String featureId, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getFeatureValidationConfig().isEnforceInboxMessagesInput()) { + return enforceFeatureActionPayload(featureId, + featureThingModel, + messageSubject, + inputPayload, + validationConfig.getFeatureValidationConfig().isForbidNonModeledInboxMessages(), + resourcePath, + true, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateFeatureActionOutput(final ThingModel featureThingModel, + final String featureId, + final String messageSubject, + @Nullable final JsonValue outputPayload, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getFeatureValidationConfig().isEnforceInboxMessagesOutput()) { + return enforceFeatureActionPayload(featureId, + featureThingModel, + messageSubject, + outputPayload, + validationConfig.getFeatureValidationConfig().isForbidNonModeledInboxMessages(), + resourcePath, + false, + context + ); + } + return success(); + } + + @Override + public CompletionStage validateFeatureEventData(final ThingModel featureThingModel, + final String featureId, + final String messageSubject, + @Nullable final JsonValue dataPayload, + final JsonPointer resourcePath, + final ValidationContext context + ) { + if (validationConfig.getFeatureValidationConfig().isEnforceOutboxMessages()) { + return enforceFeatureEventPayload(featureId, + featureThingModel, + messageSubject, + dataPayload, + validationConfig.getFeatureValidationConfig().isForbidNonModeledOutboxMessages(), + resourcePath, + context + ); + } + return success(); + } + +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java new file mode 100644 index 0000000000..207ee4eb8f --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.eclipse.ditto.wot.validation.InternalValidation.determineDittoCategories; +import static org.eclipse.ditto.wot.validation.InternalValidation.determineDittoCategory; +import static org.eclipse.ditto.wot.validation.InternalValidation.enforceActionPayload; +import static org.eclipse.ditto.wot.validation.InternalValidation.enforceEventPayload; +import static org.eclipse.ditto.wot.validation.InternalValidation.enforcePresenceOfRequiredPropertiesUponDeletion; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureOnlyDefinedActions; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureOnlyDefinedEvents; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureOnlyDefinedProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureRequiredProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.extractRequiredTmProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.filterNonProvidedRequiredProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.success; +import static org.eclipse.ditto.wot.validation.InternalValidation.validateProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.validateProperty; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.json.JsonKey; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.wot.model.Properties; +import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.ThingModel; + +final class InternalFeatureValidation { + + private InternalFeatureValidation() { + throw new AssertionError(); + } + + static CompletableFuture forbidNonModeledFeatures(@Nullable final Features features, + final Set definedFeatureIds, + final ValidationContext context + ) { + + final Set extraFeatureIds = Optional.ofNullable(features) + .map(Features::stream) + .orElseGet(Stream::empty) + .map(Feature::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + extraFeatureIds.removeAll(definedFeatureIds); + if (!extraFeatureIds.isEmpty()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Attempting to update the Thing with feature(s) which were not " + + "defined in the model: " + extraFeatureIds); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static CompletableFuture enforcePresenceOfModeledFeatures(@Nullable final Features features, + final Set definedFeatureIds, + final ValidationContext context + ) { + final Set existingFeatures = Optional.ofNullable(features) + .map(Features::stream) + .orElseGet(Stream::empty) + .map(Feature::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + if (!existingFeatures.containsAll(definedFeatureIds)) { + final Set missingFeatureIds = new LinkedHashSet<>(definedFeatureIds); + missingFeatureIds.removeAll(existingFeatures); + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Attempting to update the Thing with missing in the model " + + "defined features: " + missingFeatureIds); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static CompletableFuture> enforceFeaturePropertiesInAllSubmodels( + final Map featureThingModels, + final Features features, + final boolean desiredProperties, + final boolean forbidNonModeledProperties, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final CompletableFuture> enforcedPropertiesListFuture; + final List> enforcedPropertiesFutures = featureThingModels + .entrySet() + .stream() + .filter(entry -> features.getFeature(entry.getKey()).isPresent()) + .map(entry -> + enforceFeatureProperties(entry.getValue(), + features.getFeature(entry.getKey()).orElseThrow(), + desiredProperties, + forbidNonModeledProperties, + resourcePath.append(Thing.JsonFields.FEATURES.getPointer()) + .addLeaf(JsonKey.of(entry.getKey())) + .append(desiredProperties ? + Feature.JsonFields.DESIRED_PROPERTIES.getPointer() : + Feature.JsonFields.PROPERTIES.getPointer() + ), + context + ) + ) + .toList(); + enforcedPropertiesListFuture = + CompletableFuture.allOf(enforcedPropertiesFutures.toArray(new CompletableFuture[0])) + .thenApply(ignored -> enforcedPropertiesFutures.stream() + .map(CompletableFuture::join) + .toList() + ); + return enforcedPropertiesListFuture; + } + + static CompletableFuture enforceFeatureProperties(final ThingModel featureThingModel, + final Feature feature, + final boolean desiredProperties, + final boolean forbidNonModeledProperties, + final JsonPointer resourcePath, + final ValidationContext context + ) { + return featureThingModel.getProperties() + .map(tdProperties -> { + final FeatureProperties featureProperties; + if (desiredProperties) { + featureProperties = feature.getDesiredProperties() + .orElseGet(() -> FeatureProperties.newBuilder().build()); + } else { + featureProperties = feature.getProperties() + .orElseGet(() -> FeatureProperties.newBuilder().build()); + } + + final String containerNamePrefix = "Feature <" + feature.getId() + ">'s " + + (desiredProperties ? "desired " : ""); + final String containerNamePlural = containerNamePrefix + "properties"; + + final CompletableFuture ensureRequiredPropertiesStage; + if (!desiredProperties) { + ensureRequiredPropertiesStage = ensureRequiredProperties(featureThingModel, + tdProperties, + featureProperties, + containerNamePlural, + containerNamePrefix + "property", + resourcePath, + true, + context + ); + } else { + ensureRequiredPropertiesStage = success(); + } + + final CompletableFuture ensureOnlyDefinedPropertiesStage; + if (forbidNonModeledProperties) { + ensureOnlyDefinedPropertiesStage = ensureOnlyDefinedProperties(featureThingModel, + tdProperties, + featureProperties, + containerNamePlural, + true, + context + ); + } else { + ensureOnlyDefinedPropertiesStage = success(); + } + + final CompletableFuture validatePropertiesStage = validateProperties(featureThingModel, + tdProperties, + featureProperties, + !desiredProperties, + containerNamePlural, + resourcePath, + true, + context + ); + + return CompletableFuture.allOf( + ensureRequiredPropertiesStage, + ensureOnlyDefinedPropertiesStage, + validatePropertiesStage + ); + }).orElseGet(InternalValidation::success); + } + + static CompletableFuture enforceFeatureProperty(final ThingModel featureThingModel, + final String featureId, + final JsonPointer propertyPath, + final JsonValue propertyValue, + final boolean desiredProperty, + final boolean forbidNonModeledProperties, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final Set categories = determineDittoCategories(featureThingModel); + final boolean isCategoryUpdate = propertyPath.getLevelCount() == 1 && + categories.contains(propertyPath.getRoot().orElseThrow().toString()); + + return featureThingModel.getProperties() + .map(tdProperties -> { + final String containerNamePrefix = "Feature <" + featureId + ">'s " + + (desiredProperty ? "desired " : ""); + final String containerNamePlural = containerNamePrefix + "properties"; + + final CompletableFuture ensureOnlyDefinedPropertiesStage = + enforceFeaturePropertyOnlyDefinedProperties( + featureThingModel, propertyPath, propertyValue, forbidNonModeledProperties, context, + tdProperties, isCategoryUpdate, containerNamePlural + ); + + final CompletableFuture validatePropertiesStage = + enforceFeaturePropertyValidateProperties( + featureThingModel, propertyPath, propertyValue, desiredProperty, resourcePath, + context, tdProperties, isCategoryUpdate, containerNamePrefix + ); + + return CompletableFuture.allOf( + ensureOnlyDefinedPropertiesStage, + validatePropertiesStage + ); + }).orElseGet(InternalValidation::success); + } + + private static CompletableFuture enforceFeaturePropertyOnlyDefinedProperties( + final ThingModel featureThingModel, + final JsonPointer propertyPath, + final JsonValue propertyValue, + final boolean forbidNonModeledProperties, + final ValidationContext context, + final Properties tdProperties, + final boolean isCategoryUpdate, + final String containerNamePlural + ) { + if (isCategoryUpdate) { + final String dittoCategory = propertyPath.getRoot().orElseThrow().toString(); + if (forbidNonModeledProperties) { + final Properties propertiesInCategory = Properties.from(tdProperties.values().stream() + .filter(property -> + determineDittoCategory(featureThingModel, property) + .filter(cat -> cat.equals(dittoCategory)) + .isPresent() + ) + .toList()); + final FeatureProperties featureProperties = FeatureProperties.newBuilder() + .setAll(propertyValue.isObject() ? propertyValue.asObject() : JsonObject.empty()) + .build(); + return ensureOnlyDefinedProperties(featureThingModel, + propertiesInCategory, + featureProperties, + containerNamePlural, + false, + context + ); + } + } else { + if (forbidNonModeledProperties) { + final FeatureProperties featureProperties = FeatureProperties.newBuilder() + .set(propertyPath, propertyValue) + .build(); + return ensureOnlyDefinedProperties(featureThingModel, + tdProperties, + featureProperties, + containerNamePlural, + true, + context + ); + } + } + return success(); + } + + private static CompletableFuture enforceFeaturePropertyValidateProperties(final ThingModel featureThingModel, + final JsonPointer propertyPath, + final JsonValue propertyValue, + final boolean desiredProperty, + final JsonPointer resourcePath, + final ValidationContext context, + final Properties tdProperties, + final boolean isCategoryUpdate, + final String containerNamePrefix + ) { + if (isCategoryUpdate) { + final String dittoCategory = propertyPath.getRoot().orElseThrow().toString(); + if (!propertyValue.isObject()) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not update Feature property category " + + "<" + dittoCategory + "> as its value was not a JSON object"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + + final List sameCategoryProperties = tdProperties.values().stream() + .filter(property -> + // gather all properties from the same category + determineDittoCategory(featureThingModel, property) + .filter(cat -> cat.equals(dittoCategory)) + .isPresent() + ) + .toList(); + + if (!sameCategoryProperties.isEmpty() && propertyValue.isObject()) { + return validatePropertyCategory(featureThingModel, + Properties.from(sameCategoryProperties), + propertyPath, + !desiredProperty, + propertyValue.asObject(), + containerNamePrefix + "property", + resourcePath, + context + ); + } else { + return validateProperty(featureThingModel, + tdProperties, + propertyPath, + !desiredProperty, + propertyValue, + containerNamePrefix + "property <" + propertyPath + ">", + resourcePath, + true, + determineDittoCategories(featureThingModel), + context + ); + } + } else { + return validateProperty(featureThingModel, + tdProperties, + propertyPath, + !desiredProperty, + propertyValue, + containerNamePrefix + "property <" + propertyPath + ">", + resourcePath, + true, + determineDittoCategories(featureThingModel), + context + ); + } + } + + static CompletableFuture enforcePresenceOfRequiredPropertiesUponFeatureLevelDeletion( + final ThingModel featureThingModel, + final String featureId, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final JsonPointer propertiesPath = + resourcePath.getSubPointer(2).orElse(resourcePath); // cut /features/ + + final CompletableFuture firstStage; + if (propertiesPath.getLevelCount() > 1) { + firstStage = enforcePresenceOfRequiredPropertiesUponPropertyCategoryDeletion(featureThingModel, + featureId, + context, + propertiesPath + ); + } else { + firstStage = success(); + } + + return firstStage.thenCompose(unused -> + enforcePresenceOfRequiredPropertiesUponDeletion( + featureThingModel, + propertiesPath, + true, + determineDittoCategories(featureThingModel), + "all Feature <" + featureId + "> properties", + "Feature <" + featureId + "> property", + context + ) + ); + } + + private static CompletableFuture enforcePresenceOfRequiredPropertiesUponPropertyCategoryDeletion( + final ThingModel featureThingModel, + final String featureId, + final ValidationContext context, + final JsonPointer propertiesPath + ) { + final Set categories = determineDittoCategories(featureThingModel); + + final String potentialCategory = propertiesPath.get(1).orElseThrow().toString(); + final boolean isCategoryUpdate = propertiesPath.getLevelCount() == 2 && categories.contains(potentialCategory); + if (isCategoryUpdate) { + // handle deleting whole category like "/properties/status" - must not be allowed if it contains required properties + final boolean containsRequiredProperties = featureThingModel.getProperties() + .map(properties -> Properties.from(properties.values().stream() + .filter(property -> determineDittoCategory(featureThingModel, property) + .filter(potentialCategory::equals) + .isPresent() + ) + .toList() + ) + ) + .map(properties -> extractRequiredTmProperties(properties, featureThingModel)) + .map(map -> !map.isEmpty()) + .orElse(false); + if (containsRequiredProperties) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not delete Feature <" + featureId + "> properties " + + "category <" + potentialCategory + "> as it contains non-optional properties"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + } + return success(); + } + + static CompletableFuture enforceFeatureActionPayload(final String featureId, + final ThingModel featureThingModel, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final boolean forbidNonModeledInboxMessages, + final JsonPointer resourcePath, + final boolean isInput, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (forbidNonModeledInboxMessages) { + firstStage = ensureOnlyDefinedActions(featureThingModel.getActions().orElse(null), + messageSubject, + "Feature <" + featureId + ">'s", + context + ); + } else { + firstStage = success(); + } + return firstStage.thenCompose(unused -> + enforceActionPayload(featureThingModel, messageSubject, inputPayload, resourcePath, isInput, + "Feature <" + featureId + ">'s action <" + messageSubject + "> " + + (isInput ? "input" : "output"), + context + ) + ); + } + + static CompletableFuture enforceFeatureEventPayload(final String featureId, + final ThingModel featureThingModel, + final String messageSubject, + @Nullable final JsonValue payload, + final boolean forbidNonModeledOutboxMessages, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (forbidNonModeledOutboxMessages) { + firstStage = ensureOnlyDefinedEvents( + featureThingModel.getEvents().orElse(null), + messageSubject, + "Feature <" + featureId + ">'s", + context + ); + } else { + firstStage = success(); + } + return firstStage.thenCompose(unused -> + enforceEventPayload(featureThingModel, messageSubject, payload, resourcePath, + "Feature <" + featureId + ">'s event <" + messageSubject + "> data", + context + ) + ); + } + + static CompletableFuture validatePropertyCategory(final ThingModel featureThingModel, + final Properties categoryProperties, + final JsonPointer propertyPath, + final boolean validateRequiredObjectFields, + final JsonObject categoryObject, + final String propertyDescription, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final Map nonProvidedRequiredProperties = + filterNonProvidedRequiredProperties(categoryProperties, featureThingModel, categoryObject, false); + final JsonKey category = propertyPath.getRoot().orElseThrow(); + final String propertyCategoryDescription = propertyDescription + " category <" + category + ">"; + if (validateRequiredObjectFields && !nonProvidedRequiredProperties.isEmpty()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Required JSON fields were missing from the " + propertyCategoryDescription); + nonProvidedRequiredProperties.forEach((rpKey, requiredProperty) -> + exceptionBuilder.addValidationDetail( + resourcePath.addLeaf(JsonKey.of(rpKey)), + List.of(propertyDescription + " category <" + category + + ">'s <" + rpKey + "> is non optional and must be provided") + ) + ); + return CompletableFuture + .failedFuture(exceptionBuilder.dittoHeaders(context.dittoHeaders()).build()); + } + + return validateProperties( + featureThingModel, + categoryProperties, + categoryObject, + validateRequiredObjectFields, + propertyCategoryDescription, + resourcePath, + false, + context + ); + } + +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalThingValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalThingValidation.java new file mode 100644 index 0000000000..c44f5ab706 --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalThingValidation.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.eclipse.ditto.wot.validation.InternalValidation.enforceActionPayload; +import static org.eclipse.ditto.wot.validation.InternalValidation.enforceEventPayload; +import static org.eclipse.ditto.wot.validation.InternalValidation.enforcePresenceOfRequiredPropertiesUponDeletion; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureOnlyDefinedActions; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureOnlyDefinedEvents; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureOnlyDefinedProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.ensureRequiredProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.success; +import static org.eclipse.ditto.wot.validation.InternalValidation.validateProperties; +import static org.eclipse.ditto.wot.validation.InternalValidation.validateProperty; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.wot.model.ThingModel; + +final class InternalThingValidation { + + private InternalThingValidation() { + throw new AssertionError(); + } + + static CompletableFuture enforceThingAttributes(final ThingModel thingModel, + final Attributes attributes, + final boolean forbidNonModeledAttributes, + final JsonPointer resourcePath, + final ValidationContext context + ) { + return thingModel.getProperties() + .map(tdProperties -> { + final String containerNamePlural = "Thing's attributes"; + final CompletableFuture ensureRequiredPropertiesStage = ensureRequiredProperties(thingModel, + tdProperties, + attributes, + containerNamePlural, + "Thing's attribute", + resourcePath, + false, + context + ); + + final CompletableFuture ensureOnlyDefinedPropertiesStage; + if (forbidNonModeledAttributes) { + ensureOnlyDefinedPropertiesStage = ensureOnlyDefinedProperties(thingModel, + tdProperties, + attributes, + containerNamePlural, + false, + context + ); + } else { + ensureOnlyDefinedPropertiesStage = success(); + } + + final CompletableFuture validatePropertiesStage = validateProperties(thingModel, + tdProperties, + attributes, + true, + containerNamePlural, + resourcePath, + false, + context + ); + + return CompletableFuture.allOf( + ensureRequiredPropertiesStage, + ensureOnlyDefinedPropertiesStage, + validatePropertiesStage + ); + }).orElseGet(InternalValidation::success); + } + + static CompletableFuture enforceThingAttribute(final ThingModel thingModel, + final JsonPointer attributePath, + final JsonValue attributeValue, + final boolean forbidNonModeledAttributes, + final JsonPointer resourcePath, + final ValidationContext context + ) { + return thingModel.getProperties() + .map(tdProperties -> { + final Attributes attributes = Attributes.newBuilder().set(attributePath, attributeValue).build(); + final CompletableFuture ensureOnlyDefinedPropertiesStage; + if (forbidNonModeledAttributes) { + ensureOnlyDefinedPropertiesStage = ensureOnlyDefinedProperties(thingModel, + tdProperties, + attributes, + "Thing's attributes", + false, + context + ); + } else { + ensureOnlyDefinedPropertiesStage = success(); + } + + final CompletableFuture validatePropertiesStage = validateProperty(thingModel, + tdProperties, + attributePath, + true, + attributeValue, + "Thing's attribute <" + attributePath + ">", resourcePath, + false, + Set.of(), + context + ); + + return CompletableFuture.allOf( + ensureOnlyDefinedPropertiesStage, + validatePropertiesStage + ); + }).orElseGet(InternalValidation::success); + } + + static CompletableFuture enforcePresenceOfRequiredPropertiesUponThingLevelDeletion( + final ThingModel thingModel, + final JsonPointer resourcePath, + final ValidationContext context + ) { + return enforcePresenceOfRequiredPropertiesUponDeletion( + thingModel, + resourcePath, + false, + Set.of(), + "all Thing attributes", + "Thing attribute", + context + ); + } + + static CompletableFuture enforceThingActionPayload(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue payload, + final boolean forbidNonModeledInboxMessages, + final JsonPointer resourcePath, + final boolean isInput, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (isInput && forbidNonModeledInboxMessages) { + firstStage = ensureOnlyDefinedActions(thingModel.getActions().orElse(null), + messageSubject, + "Thing's", + context + ); + } else { + firstStage = success(); + } + return firstStage.thenCompose(unused -> + enforceActionPayload(thingModel, messageSubject, payload, resourcePath, isInput, + "Thing's action <" + messageSubject + "> " + (isInput ? "input" : "output"), + context + ) + ); + } + + static CompletableFuture enforceThingEventPayload(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue payload, + final boolean forbidNonModeledOutboxMessages, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final CompletableFuture firstStage; + if (forbidNonModeledOutboxMessages) { + firstStage = ensureOnlyDefinedEvents(thingModel.getEvents().orElse(null), + messageSubject, "Thing's", context); + } else { + firstStage = success(); + } + return firstStage.thenCompose(unused -> + enforceEventPayload(thingModel, messageSubject, payload, resourcePath, + "Thing's event <" + messageSubject + "> data", context + ) + ); + } + +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java new file mode 100644 index 0000000000..64cf5b2fac --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java @@ -0,0 +1,639 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import java.util.AbstractMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.json.JsonKey; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.wot.model.Actions; +import org.eclipse.ditto.wot.model.DittoWotExtension; +import org.eclipse.ditto.wot.model.Event; +import org.eclipse.ditto.wot.model.Events; +import org.eclipse.ditto.wot.model.ObjectSchema; +import org.eclipse.ditto.wot.model.Properties; +import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.SingleDataSchema; +import org.eclipse.ditto.wot.model.SingleUriAtContext; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.TmOptionalElement; + +import com.networknt.schema.output.OutputUnit; + +final class InternalValidation { + + private static final JsonSchemaTools JSON_SCHEMA_TOOLS = new JsonSchemaTools(); + private static final String PROPERTIES_PATH_PREFIX = "/properties/"; + + private InternalValidation() { + throw new AssertionError(); + } + + static CompletableFuture ensureOnlyDefinedProperties(final ThingModel thingModel, + final Properties tdProperties, + final JsonObject propertiesContainer, + final String containerNamePlural, + final boolean handleDittoCategory, + final ValidationContext context + ) { + final Set allDefinedPropertyKeys = tdProperties.keySet(); + final Set allAvailablePropertiesKeys = + propertiesContainer.getKeys().stream().map(JsonKey::toString) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (handleDittoCategory) { + final Set categories = determineDittoCategories(thingModel); + categories.forEach(category -> + propertiesContainer.getValue(category) + .map(JsonValue::asObject) + .ifPresent(categoryObj -> + allAvailablePropertiesKeys.addAll( + categoryObj.getKeys().stream() + .map(JsonKey::toString) + .map(key -> category + "/" + key) + .collect(Collectors.toCollection(LinkedHashSet::new)) + ) + ) + ); + tdProperties.forEach((propertyName, property) -> { + final Optional dittoCategory = determineDittoCategory(thingModel, property); + final String categorizedPropertyName = dittoCategory + .map(c -> c + "/").orElse("") + .concat(propertyName); + if (propertiesContainer.contains(JsonPointer.of(categorizedPropertyName))) { + allAvailablePropertiesKeys.remove(categorizedPropertyName); + dittoCategory.ifPresent(allAvailablePropertiesKeys::remove); + } else if (dittoCategory.filter(propertiesContainer::contains).isPresent()) { + allAvailablePropertiesKeys.remove(dittoCategory.get()); + } + }); + } else { + allAvailablePropertiesKeys.removeAll(allDefinedPropertyKeys); + } + + if (!allAvailablePropertiesKeys.isEmpty()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("The " + containerNamePlural + " contained " + + "JSON fields which were not defined in the model: " + allAvailablePropertiesKeys); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static CompletableFuture ensureRequiredProperties(final ThingModel thingModel, + final Properties tdProperties, + final JsonObject propertiesContainer, + final String containerNamePlural, + final String containerName, + final JsonPointer resourcePath, + final boolean handleDittoCategory, + final ValidationContext context + ) { + final Map nonProvidedRequiredProperties = + filterNonProvidedRequiredProperties(tdProperties, thingModel, propertiesContainer, handleDittoCategory); + + if (!nonProvidedRequiredProperties.isEmpty()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Required JSON fields were missing from the " + containerNamePlural); + nonProvidedRequiredProperties.forEach((rpKey, requiredProperty) -> + { + JsonPointer fullPointer = resourcePath; + final Optional dittoCategory = determineDittoCategory(thingModel, requiredProperty); + if (handleDittoCategory && dittoCategory.isPresent()) { + fullPointer = fullPointer.addLeaf(JsonKey.of(dittoCategory.get())); + } + fullPointer = fullPointer.addLeaf(JsonKey.of(rpKey)); + exceptionBuilder.addValidationDetail( + fullPointer, + List.of(containerName + " <" + rpKey + "> is non optional and must be provided") + ); + } + ); + return CompletableFuture.failedFuture(exceptionBuilder.dittoHeaders(context.dittoHeaders()).build()); + } + return success(); + } + + static CompletableFuture enforcePresenceOfRequiredPropertiesUponDeletion( + final ThingModel thingModel, + final JsonPointer resourcePath, + final boolean handleDittoCategory, + final Set dittoCategories, + final String pluralDescription, + final String singularDescription, + final ValidationContext context + ) { + if (resourcePath.getLevelCount() == 1) { + // deleting all attributes/properties .. + // must be prevented if there is at least one non-optional TM model "property" defined + final boolean containsRequiredProperties = thingModel.getProperties() + .map(properties -> extractRequiredTmProperties(properties, thingModel)) + .map(map -> !map.isEmpty()) + .orElse(false); + if (containsRequiredProperties) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not delete " + pluralDescription + ", " + + "as there are some defined as non-optional in the model"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + } else { + // deleting a specific attribute/property .. + // check if that is an optional one + final JsonPointer resourcePointer = resourcePath.getSubPointer(1).orElseThrow(); + final Optional propertyWithCategory = findPropertyBasedOnPath(thingModel, + thingModel.getProperties().orElse(Properties.of(Map.of())), + resourcePointer, + handleDittoCategory, + dittoCategories + ); + + final boolean isPropertyRequired; + if (propertyWithCategory.isPresent() && ( + (propertyWithCategory.get().category() != null && resourcePointer.getLevelCount() == 2) || + (propertyWithCategory.get().category() == null && resourcePointer.getLevelCount() == 1) + )) { + // it is sufficient to only look at the WoT TM Property + isPropertyRequired = propertyWithCategory + .filter(withCategory -> isTmPropertyRequired(withCategory.property(), thingModel)) + .isPresent(); + } else { + // we need to look into "required" of property schemas of type "object" to find out if the path is required + isPropertyRequired = propertyWithCategory + .filter(withCategory -> + isPropertyRequired(withCategory.property(), + withCategory.category(), + resourcePointer, + handleDittoCategory + ) + ).isPresent(); + } + if (isPropertyRequired) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not delete " + singularDescription + " <" + resourcePointer + "> " + + "as it is defined as non-optional in the model"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + } + return success(); + } + + static boolean isPropertyRequired(final Property property, + @Nullable final String category, + final JsonPointer resourcePath, + final boolean handleDittoCategory + ) { + if (handleDittoCategory) { + return Optional.ofNullable(category) + .map(cat -> + resourcePath.getRoot().filter(root -> root.toString().equals(cat)).isPresent() && + checkResourceForRequired(resourcePath.getSubPointer(1).orElse(null), property, + property.getPropertyName()) + ) + .orElseGet(() -> + checkResourceForRequired(resourcePath, property, property.getPropertyName()) + ); + } else { + return checkResourceForRequired(resourcePath, property, property.getPropertyName()); + } + } + + private static boolean checkResourceForRequired(@Nullable final JsonPointer resourcePath, + final Property property, + final String key + ) { + if (null == resourcePath || resourcePath.isEmpty()) { + return false; + } else if (resourcePath.getLevelCount() == 1) { + return resourcePath.getRoot().filter(prop -> prop.toString().equals(key)).isPresent(); + } else if (property.isObjectSchema()) { + return checkResourceForRequired(resourcePath.getSubPointer(1).orElseThrow(), property.asObjectSchema()); + } else { + // this should not happen, consider it an error? + return false; + } + } + + private static boolean checkResourceForRequired(@Nullable final JsonPointer resourcePath, + final ObjectSchema objectSchema + ) { + if (null == resourcePath || resourcePath.isEmpty()) { + return false; + } else { + final Optional rootKey = resourcePath.getRoot().map(JsonKey::toString); + final boolean isRequired = rootKey + .map(key -> objectSchema.getRequired().contains(key)) + .orElse(false); + if (resourcePath.getLevelCount() > 1 && isRequired) { + final SingleDataSchema subSchema = objectSchema.getProperties().get(rootKey.orElseThrow()); + // recurse! + return checkResourceForRequired(resourcePath.getSubPointer(1).orElseThrow(), (ObjectSchema) subSchema); + } else { + return isRequired; + } + } + } + + static Map filterNonProvidedRequiredProperties(final Properties tdProperties, + final ThingModel thingModel, + final JsonObject propertiesContainer, + final boolean handleDittoCategory + ) { + final Map requiredProperties = extractRequiredTmProperties(tdProperties, thingModel); + final Map nonProvidedRequiredProperties = new LinkedHashMap<>(requiredProperties); + if (handleDittoCategory) { + requiredProperties.forEach((rpKey, requiredProperty) -> { + final Optional dittoCategory = determineDittoCategory(thingModel, requiredProperty); + if (dittoCategory.isPresent()) { + propertiesContainer.getValue(dittoCategory.get()) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .ifPresent(categorizedProperties -> categorizedProperties.getKeys().stream() + .map(JsonKey::toString) + .forEach(nonProvidedRequiredProperties::remove) + ); + } else { + propertiesContainer.getKeys().stream() + .map(JsonKey::toString) + .forEach(nonProvidedRequiredProperties::remove); + } + }); + } else { + propertiesContainer.getKeys().stream() + .map(JsonKey::toString) + .forEach(nonProvidedRequiredProperties::remove); + } + return nonProvidedRequiredProperties; + } + + static Map extractRequiredTmProperties(final Properties tdProperties, + final ThingModel thingModel + ) { + return thingModel.getTmOptional().map(tmOptionalElements -> { + final Map allRequiredProperties = new LinkedHashMap<>(tdProperties); + tmOptionalElements.stream() + .map(TmOptionalElement::toString) + .filter(el -> el.startsWith(PROPERTIES_PATH_PREFIX)) + .map(el -> el.replace(PROPERTIES_PATH_PREFIX, "")) + .forEach(allRequiredProperties::remove); + return allRequiredProperties; + }).orElse(tdProperties); + } + + static boolean isTmPropertyRequired(final Property property, + final ThingModel thingModel + ) { + return thingModel.getTmOptional() + .map(tmOptionalElements -> tmOptionalElements.stream() + .map(TmOptionalElement::toString) + .filter(el -> el.startsWith(PROPERTIES_PATH_PREFIX)) + .map(el -> el.replace(PROPERTIES_PATH_PREFIX, "")) + .noneMatch(el -> property.getPropertyName().equals(el)) + ).orElse(false); + } + + static CompletableFuture ensureOnlyDefinedActions(@Nullable final Actions actions, + final String messageSubject, + final String containerName, + final ValidationContext context + ) { + final Set allDefinedActionKeys = Optional.ofNullable(actions).map(Actions::keySet).orElseGet(Set::of); + final boolean messageSubjectIsDefinedAsAction = allDefinedActionKeys.contains(messageSubject); + if (!messageSubjectIsDefinedAsAction) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("The " + containerName + " message subject <" + + messageSubject + "> is not defined as known action in the model: " + allDefinedActionKeys + ); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static CompletableFuture ensureOnlyDefinedEvents( + @Nullable final Events events, + final String messageSubject, + final String containerName, + final ValidationContext context + ) { + final Set allDefinedEventKeys = Optional.ofNullable(events).map(Events::keySet).orElseGet(Set::of); + final boolean messageSubjectIsDefinedAsEvent = allDefinedEventKeys.contains(messageSubject); + if (!messageSubjectIsDefinedAsEvent) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("The " + containerName + " message subject <" + + messageSubject + "> is not defined as known event in the model: " + allDefinedEventKeys + ); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static Set determineDittoCategories(final ThingModel thingModel) { + return determineDittoCategories(thingModel, thingModel.getProperties().orElse(Properties.of(Map.of()))); + } + + static Set determineDittoCategories(final ThingModel thingModel, final Properties properties) { + final Optional dittoExtensionPrefix = thingModel.getAtContext() + .determinePrefixFor(SingleUriAtContext.DITTO_WOT_EXTENSION); + return dittoExtensionPrefix.stream().flatMap(prefix -> + properties.values().stream().flatMap(jsonFields -> + jsonFields.getValue(prefix + ":" + DittoWotExtension.DITTO_WOT_EXTENSION_CATEGORY) + .filter(JsonValue::isString) + .map(JsonValue::asString) + .stream() + ) + ).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + static Optional determineDittoCategory(final ThingModel thingModel, final Property property) { + final Optional dittoExtensionPrefix = thingModel.getAtContext() + .determinePrefixFor(SingleUriAtContext.DITTO_WOT_EXTENSION); + return dittoExtensionPrefix.flatMap(prefix -> + property.getValue(prefix + ":" + DittoWotExtension.DITTO_WOT_EXTENSION_CATEGORY) + ) + .filter(JsonValue::isString) + .map(JsonValue::asString); + } + + static CompletableFuture validateProperties(final ThingModel thingModel, + final Properties tdProperties, + final JsonObject propertiesContainer, + final boolean validateRequiredObjectFields, + final String containerNamePlural, + final JsonPointer resourcePath, + final boolean handleDittoCategory, + final ValidationContext context + ) { + final Map invalidProperties; + if (handleDittoCategory) { + invalidProperties = determineInvalidProperties(tdProperties, + p -> propertiesContainer.getValue( + determineDittoCategory(thingModel, p) + .map(c -> c + "/") + .orElse("") + .concat(p.getPropertyName()) + ), + validateRequiredObjectFields, + context + ); + } else { + invalidProperties = determineInvalidProperties(tdProperties, + p -> propertiesContainer.getValue(p.getPropertyName()), + validateRequiredObjectFields, + context + ); + } + + if (!invalidProperties.isEmpty()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("The " + containerNamePlural + " contained validation errors, " + + "check the validation details."); + invalidProperties.forEach((property, validationOutputUnit) -> { + JsonPointer fullPointer = resourcePath; + final Optional dittoCategory = determineDittoCategory(thingModel, property); + if (handleDittoCategory && dittoCategory.isPresent()) { + fullPointer = fullPointer.addLeaf(JsonKey.of(dittoCategory.get())); + } + fullPointer = fullPointer.addLeaf(JsonKey.of(property.getPropertyName())); + exceptionBuilder.addValidationDetail( + fullPointer, + validationOutputUnit.getDetails().stream() + .map(ou -> ou.getInstanceLocation() + ": " + ou.getErrors()) + .toList() + ); + }); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static Map determineInvalidProperties(final Properties tdProperties, + final Function> propertyExtractor, + final boolean validateRequiredObjectFields, + final ValidationContext context + ) { + return tdProperties.entrySet().stream() + .flatMap(tdPropertyEntry -> + propertyExtractor.apply(tdPropertyEntry.getValue()) + .map(propertyValue -> new AbstractMap.SimpleEntry<>( + tdPropertyEntry.getValue(), + JSON_SCHEMA_TOOLS.validateDittoJsonBasedOnDataSchema( + tdPropertyEntry.getValue(), + JsonPointer.empty(), + validateRequiredObjectFields, + propertyValue, + context.dittoHeaders() + ) + )) + .filter(entry -> !entry.getValue().isValid()) + .stream() + ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (u, v) -> { + throw new IllegalStateException(String.format("Duplicate key %s", u)); + }, LinkedHashMap::new)); + } + + static CompletableFuture validateProperty(final ThingModel thingModel, + final Properties tdProperties, + final JsonPointer propertyPath, + final boolean validateRequiredObjectFields, + final JsonValue propertyValue, + final String propertyDescription, + final JsonPointer resourcePath, + final boolean handleDittoCategory, + final Set dittoCategories, + final ValidationContext context + ) { + return findPropertyBasedOnPath(thingModel, tdProperties, propertyPath, handleDittoCategory, dittoCategories) + .map(propertyWithCategory -> { + final JsonValue valueToValidate; + if (propertyPath.getLevelCount() > 1) { + final int level; + if (handleDittoCategory && propertyWithCategory.category() != null) { + level = 2; + } else { + level = 1; + } + if (propertyPath.getLevelCount() > level) { + valueToValidate = JsonObject.newBuilder() + .set(propertyPath.getSubPointer(level).orElseThrow(), propertyValue) + .build(); + } else { + valueToValidate = propertyValue; + } + } else { + valueToValidate = propertyValue; + } + + if (handleDittoCategory) { + final Optional dittoCategory = Optional.ofNullable(propertyWithCategory.category()); + final JsonPointer thePropertyPath = dittoCategory + .flatMap(cat -> propertyPath.getSubPointer(1)) + .orElse(propertyPath); + return validateSingleDataSchema( + propertyWithCategory.property(), + propertyDescription, + thePropertyPath, + validateRequiredObjectFields, + valueToValidate, + resourcePath, + context + ); + } else { + return validateSingleDataSchema( + propertyWithCategory.property(), + propertyDescription, + propertyPath, + validateRequiredObjectFields, + valueToValidate, + resourcePath, + context + ); + } + }).orElseGet(InternalValidation::success); + } + + private static Optional findPropertyBasedOnPath(final ThingModel thingModel, + final Properties tdProperties, + final JsonPointer propertyPath, + final boolean handleDittoCategory, + final Set dittoCategories + ) { + if (handleDittoCategory) { + return dittoCategories.stream() + .filter(category -> propertyPath.getRoot().orElseThrow().toString().equals(category)) + .filter(category -> propertyPath.getLevelCount() > 1) + .map(category -> + tdProperties.getProperty(propertyPath.get(1).orElseThrow().toString()) + .filter(p -> determineDittoCategory(thingModel, p) + .filter(category::equals) + .isPresent() + ) + .map(p -> new PropertyWithCategory(p, category)) + ) + .findAny() + .orElseGet(() -> tdProperties.getProperty(propertyPath.getRoot().orElseThrow().toString()) + .map(p -> new PropertyWithCategory(p, null))); + } else { + return tdProperties.getProperty(propertyPath.getRoot().orElseThrow().toString()) + .map(p -> new PropertyWithCategory(p, null)); + } + } + + static CompletableFuture enforceActionPayload(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue inputPayload, + final JsonPointer resourcePath, + final boolean isInput, + final String validationFailedDescription, + final ValidationContext context + ) { + return thingModel.getActions() + .flatMap(action -> action.getAction(messageSubject)) + .flatMap(action -> isInput ? action.getInput() : action.getOutput()) + .map(schema -> validateSingleDataSchema( + schema, + validationFailedDescription, + JsonPointer.empty(), + true, + inputPayload, + resourcePath, + context + )) + .orElseGet(InternalValidation::success); + } + + static CompletableFuture enforceEventPayload(final ThingModel thingModel, + final String messageSubject, + @Nullable final JsonValue dataPayload, + final JsonPointer resourcePath, + final String validationFailedDescription, + final ValidationContext context + ) { + return thingModel.getEvents() + .flatMap(event -> event.getEvent(messageSubject)) + .flatMap(Event::getData) + .map(schema -> validateSingleDataSchema( + schema, + validationFailedDescription, + JsonPointer.empty(), + true, + dataPayload, + resourcePath, + context + )) + .orElseGet(InternalValidation::success); + } + + static CompletableFuture validateSingleDataSchema(final SingleDataSchema dataSchema, + final String validatedDescription, + final JsonPointer pointerPath, + final boolean validateRequiredObjectFields, + @Nullable final JsonValue jsonValue, + final JsonPointer resourcePath, + final ValidationContext context + ) { + final OutputUnit validationOutput = JSON_SCHEMA_TOOLS.validateDittoJsonBasedOnDataSchema( + dataSchema, + pointerPath, + validateRequiredObjectFields, + jsonValue, + context.dittoHeaders() + ); + + if (!validationOutput.isValid()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("The " + validatedDescription + " contained validation errors, " + + "check the validation details."); + exceptionBuilder.addValidationDetail( + resourcePath, + validationOutput.getDetails().stream() + .map(ou -> ou.getInstanceLocation() + ": " + ou.getErrors()) + .toList() + ); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + return success(); + } + + static CompletableFuture success() { + return CompletableFuture.completedFuture(null); + } + + record PropertyWithCategory(Property property, @Nullable String category) {} +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/JsonSchemaTools.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/JsonSchemaTools.java new file mode 100644 index 0000000000..4b91d662b5 --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/JsonSchemaTools.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.internal.utils.json.CborFactoryLoader; +import org.eclipse.ditto.json.CborFactory; +import org.eclipse.ditto.json.JsonCollectors; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.wot.model.ArraySchema; +import org.eclipse.ditto.wot.model.DataSchemaType; +import org.eclipse.ditto.wot.model.ObjectSchema; +import org.eclipse.ditto.wot.model.SingleDataSchema; +import org.eclipse.ditto.wot.model.WotInternalErrorException; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; +import com.networknt.schema.JsonMetaSchema; +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.NonValidationKeyword; +import com.networknt.schema.OutputFormat; +import com.networknt.schema.PathType; +import com.networknt.schema.SchemaId; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.output.OutputUnit; + +/** + * Contains tools around the used JsonSchema library and validating Ditto JSON, including mapping to Jackson. + */ +final class JsonSchemaTools { + + private static final String PROPERTIES = "properties"; + + private final CborFactory cborFactory; + private final ObjectMapper jacksonCborMapper; + private final SchemaValidatorsConfig schemaValidatorsConfig; + + JsonSchemaTools() { + final var cborFactoryLoader = CborFactoryLoader.getInstance(); + cborFactory = cborFactoryLoader.getCborFactoryOrThrow(); + jacksonCborMapper = new CBORMapper(); + schemaValidatorsConfig = SchemaValidatorsConfig.builder() + .pathType(PathType.JSON_POINTER) + .build(); + } + + JsonSchema extractFromSingleDataSchema(final SingleDataSchema dataSchema, + final boolean validateRequiredObjectFields, + final DittoHeaders dittoHeaders + ) { + final JsonNode jsonNode; + try { + final JsonObject dataSchemaJson; + if (!validateRequiredObjectFields) { + dataSchemaJson = adjustDataSchemaRemovingRequiredObjectFields(dataSchema.toJson()); + } else { + dataSchemaJson = dataSchema.toJson(); + } + final byte[] bytes = cborFactory.toByteArray(dataSchemaJson); + jsonNode = jacksonCborMapper.reader().readTree(bytes); + } catch (final JsonParseException e) { + throw DittoRuntimeException.asDittoRuntimeException(e, t -> WotInternalErrorException.newBuilder() + .message("Error during parsing input JSON") + .cause(t) + .dittoHeaders(dittoHeaders) + .build()) + .setDittoHeaders(dittoHeaders); + } catch (final IOException e) { + throw WotInternalErrorException.newBuilder() + .cause(e) + .dittoHeaders(dittoHeaders) + .build(); + } + final JsonMetaSchema.Builder metaSchemaBuilder = JsonMetaSchema.builder(SchemaId.V7, JsonMetaSchema.getV7()); + metaSchemaBuilder.keyword(new NonValidationKeyword("@type")); + metaSchemaBuilder.keyword(new NonValidationKeyword("unit")); + metaSchemaBuilder.keyword(new NonValidationKeyword("ditto:category")); + return JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7, builder -> + builder.metaSchema(metaSchemaBuilder.build()) + ) + .getSchema(jsonNode, schemaValidatorsConfig); + } + + private static JsonObject adjustDataSchemaRemovingRequiredObjectFields(final JsonObject dataSchemaJson) { + final Optional type = dataSchemaJson.getValue(SingleDataSchema.DataSchemaJsonFields.TYPE); + if (type.filter(DataSchemaType.OBJECT.getName()::equals).isPresent()) { + final Optional adjustedProperties = dataSchemaJson.getValue(ObjectSchema.JsonFields.PROPERTIES) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(obj -> obj.stream() + .map(field -> JsonField.newInstance(field.getKey(), + adjustDataSchemaRemovingRequiredObjectFields(field.getValue().asObject())) + // recurse! + ).collect(JsonCollectors.fieldsToObject()) + ); + + JsonObject dataSchemaJsonAdjusted = dataSchemaJson; + if (adjustedProperties.isPresent()) { + dataSchemaJsonAdjusted = adjustedProperties.map( + adjProps -> dataSchemaJson.set(ObjectSchema.JsonFields.PROPERTIES, adjProps) + ) + .orElse(dataSchemaJson); + } + return dataSchemaJsonAdjusted.remove("required"); + } else if (type.filter(DataSchemaType.ARRAY.getName()::equals).isPresent()) { + final Optional adjustedItems = dataSchemaJson.getValue(ArraySchema.JsonFields.ITEMS) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(JsonSchemaTools::adjustDataSchemaRemovingRequiredObjectFields); // recurse! + + JsonObject dataSchemaJsonAdjusted = dataSchemaJson; + if (adjustedItems.isPresent()) { + dataSchemaJsonAdjusted = adjustedItems.map( + adjProps -> dataSchemaJson.set(ArraySchema.JsonFields.ITEMS, adjProps) + ) + .orElse(dataSchemaJson); + } + return dataSchemaJsonAdjusted; + } + return dataSchemaJson; + } + + OutputUnit validateDittoJsonBasedOnDataSchema(final SingleDataSchema dataSchema, + final JsonPointer pointerPath, + final boolean validateRequiredObjectFields, + @Nullable final JsonValue jsonValue, + final DittoHeaders dittoHeaders + ) { + final JsonSchema jsonSchema = + extractFromSingleDataSchema(dataSchema, validateRequiredObjectFields, dittoHeaders); + + JsonPointer relativePropertyPath = JsonPointer.empty(); + JsonSchema relativeSchema = jsonSchema; + JsonValue valueToValidate = jsonValue; + if (pointerPath.getLevelCount() > 1) { + final JsonPointer subPointer = pointerPath.getSubPointer(1).orElseThrow(); + relativePropertyPath = subPointer; + JsonNodePath jsonNodePath = new JsonNodePath(PathType.JSON_POINTER); + for (int i = 0; i < subPointer.getLevelCount(); i++) { + // adding "properties" only works if we always deal with "object" schemas .. + // which however is the only way Ditto allows to use JsonPointer notation + // accessing array elements is not supported in Ditto + final String jsonKey = subPointer.get(i).orElseThrow().toString(); + if (relativeSchema.getSchemaNode().has(PROPERTIES) && + relativeSchema.getSchemaNode().get(PROPERTIES).has(jsonKey) && + Optional.ofNullable(relativeSchema.getSchemaNode().get(PROPERTIES).get(jsonKey).get("type")) + .map(JsonNode::asText).filter("object"::equals).isPresent() + ) { + jsonNodePath = jsonNodePath + .append(PROPERTIES) + .append(jsonKey); + relativeSchema = relativeSchema.getSubSchema(jsonNodePath); + relativePropertyPath = relativePropertyPath.getSubPointer(1).orElseThrow(); + valueToValidate = Optional.ofNullable(valueToValidate) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .flatMap(obj -> obj.getValue(jsonKey)) + .orElse(valueToValidate); + } + } + } + return validateDittoJson(relativeSchema, relativePropertyPath, valueToValidate, dittoHeaders); + } + + OutputUnit validateDittoJson(final JsonSchema jsonSchema, + final JsonPointer relativePropertyPath, + @Nullable final JsonValue jsonValue, + final DittoHeaders dittoHeaders + ) { + if (jsonValue == null) { + throw WotThingModelPayloadValidationException.newBuilder("No provided JSON value to validate was present") + .dittoHeaders(dittoHeaders) + .build(); + } + + final JsonNode jsonNode; + try { + final byte[] bytes = cborFactory.toByteArray(jsonValue); + jsonNode = jacksonCborMapper.reader().readTree(bytes); + } catch (final JsonParseException e) { + throw DittoRuntimeException.asDittoRuntimeException(e, t -> WotInternalErrorException.newBuilder() + .message("Error during parsing input JSON") + .cause(t) + .dittoHeaders(dittoHeaders) + .build()) + .setDittoHeaders(dittoHeaders); + } catch (final IOException e) { + throw WotInternalErrorException.newBuilder() + .cause(e) + .dittoHeaders(dittoHeaders) + .build(); + } + final OutputUnit validate = jsonSchema.validate(jsonNode, OutputFormat.LIST); + if (!validate.isValid() && !validate.getDetails().isEmpty()) { + final List validationDetails = new ArrayList<>(validate.getDetails()); + validate.getDetails().forEach(detail -> { + if (!relativePropertyPath.isEmpty()) { + validationDetails.remove(detail); + if (detail.getInstanceLocation().startsWith(relativePropertyPath.toString()) || + detail.getEvaluationPath().startsWith( + StreamSupport.stream(relativePropertyPath.spliterator(), false) + .collect(Collectors.joining("/properties/", "/properties/", "")) + ) + ) { + final String adjustedInstanceLocation = + detail.getInstanceLocation().replace(relativePropertyPath.toString(), ""); + detail.setInstanceLocation(adjustedInstanceLocation); + validationDetails.add(detail); + } + } + }); + validate.setDetails(validationDetails); + } + return validate; + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/ValidationContext.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/ValidationContext.java new file mode 100644 index 0000000000..1e28054b9e --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/ValidationContext.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.things.model.FeatureDefinition; +import org.eclipse.ditto.things.model.ThingDefinition; + +/** + * A validation context provides the context of an API call which can be used to dynamically determine custom + * configuration overrides, e.g. based on specific {@link DittoHeaders} or specific thing or feature definitions. + * + * @param dittoHeaders the DittoHeaders of the API call + * @param thingDefinition the optional ThingDefinition of the thing to be updated by the API call + * @param featureDefinition the optional FeatureDefinition of the thing to be updated by the API call + */ +public record ValidationContext( + DittoHeaders dittoHeaders, + @Nullable ThingDefinition thingDefinition, + @Nullable FeatureDefinition featureDefinition +) { + + public static ValidationContext buildValidationContext(final DittoHeaders dittoHeaders, + @Nullable final ThingDefinition thingDefinition, + @Nullable final FeatureDefinition featureDefinition + ) { + return new ValidationContext(dittoHeaders, thingDefinition, featureDefinition); + } + + public static ValidationContext buildValidationContext(final DittoHeaders dittoHeaders, + @Nullable final ThingDefinition thingDefinition + ) { + return new ValidationContext(dittoHeaders, thingDefinition, null); + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/WotThingModelPayloadValidationException.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/WotThingModelPayloadValidationException.java new file mode 100644 index 0000000000..ac8d1610c2 --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/WotThingModelPayloadValidationException.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.io.Serial; +import java.net.URI; +import java.util.AbstractMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.NotThreadSafe; + +import org.eclipse.ditto.base.model.common.HttpStatus; +import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; +import org.eclipse.ditto.base.model.exceptions.DittoRuntimeExceptionBuilder; +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.json.FieldType; +import org.eclipse.ditto.base.model.json.JsonParsableException; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.json.JsonCollectors; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonFieldDefinition; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.wot.model.WotException; + +/** + * Exception thrown when a Ditto Thing (or parts of it), so the payload, could not be validated against the WoT Model. + * + * @since 3.6.0 + */ +@Immutable +@JsonParsableException(errorCode = WotThingModelPayloadValidationException.ERROR_CODE) +public final class WotThingModelPayloadValidationException extends DittoRuntimeException implements WotException { + + /** + * Error code of this exception. + */ + public static final String ERROR_CODE = ERROR_CODE_PREFIX + "payload.validation.error"; + + private static final String DEFAULT_MESSAGE = + "The provided payload did not conform to the specified WoT (Web of Things) model."; + + @Serial + private static final long serialVersionUID = -236554134452227841L; + + private final Map> validationDetails; + + private WotThingModelPayloadValidationException(final DittoHeaders dittoHeaders, + @Nullable final String message, + @Nullable final String description, + @Nullable final Throwable cause, + @Nullable final URI href, + final Map> validationDetails) { + super(ERROR_CODE, HttpStatus.BAD_REQUEST, dittoHeaders, message, description, cause, href); + this.validationDetails = validationDetails; + } + + /** + * A mutable builder for a {@code WotThingModelPayloadValidationException}. + * + * @param validationDescription the details about what was not valid. + * @return the builder. + * @throws NullPointerException if {@code validationDescription} is {@code null}. + */ + public static Builder newBuilder(final String validationDescription) { + return new Builder(validationDescription); + } + + /** + * Constructs a new {@code WotThingModelPayloadValidationException} object with the exception message extracted from the given + * JSON object. + * + * @param jsonObject the JSON to read the {@link org.eclipse.ditto.base.model.exceptions.DittoRuntimeException.JsonFields#MESSAGE} field from. + * @param dittoHeaders the headers of the command which resulted in this exception. + * @return the new WotThingModelPayloadValidationException. + * @throws NullPointerException if any argument is {@code null}. + * @throws org.eclipse.ditto.json.JsonMissingFieldException if this JsonObject did not contain an error message. + * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected + * format. + */ + public static WotThingModelPayloadValidationException fromJson(final JsonObject jsonObject, + final DittoHeaders dittoHeaders) { + + return DittoRuntimeException.fromJson(jsonObject, dittoHeaders, + new Builder(readValidationDetails(jsonObject)) + ); + } + + @Override + public DittoRuntimeException setDittoHeaders(final DittoHeaders dittoHeaders) { + return new Builder(validationDetails) + .message(getMessage()) + .description(getDescription().orElse(null)) + .cause(getCause()) + .href(getHref().orElse(null)) + .dittoHeaders(dittoHeaders) + .build(); + } + + private static Map> readValidationDetails(final JsonObject jsonObject) { + checkNotNull(jsonObject, "JSON object"); + return jsonObject.getValue(JsonFields.VALIDATION_DETAILS) + .map(validationDetails -> validationDetails.stream() + .map(field -> new AbstractMap.SimpleEntry<>( + JsonPointer.of(field.getKey().toString()), + field.getValue().asArray().stream().map(JsonValue::formatAsString).toList()) + ) + ).stream() + .flatMap(Function.identity()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (u, v) -> { + throw new IllegalStateException(String.format("Duplicate key %s", u)); + }, LinkedHashMap::new)); + } + + @Override + protected void appendToJson(final JsonObjectBuilder jsonObjectBuilder, final Predicate predicate) { + final JsonObject detailsObject = validationDetails.entrySet().stream() + .map(entry -> JsonField.newInstance(entry.getKey().toString(), + entry.getValue().stream() + .map(JsonValue::of) + .collect(JsonCollectors.valuesToArray()) + )) + .collect(JsonCollectors.fieldsToObject()); + if (!detailsObject.isEmpty()) { + jsonObjectBuilder.set(JsonFields.VALIDATION_DETAILS, detailsObject, predicate); + } + } + + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final WotThingModelPayloadValidationException that = (WotThingModelPayloadValidationException) o; + return Objects.equals(validationDetails, that.validationDetails); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), validationDetails); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "message='" + getMessage() + '\'' + + ", errorCode=" + getErrorCode() + + ", httpStatus=" + getHttpStatus() + + ", description='" + getDescription().orElse(null) + '\'' + + ", href=" + getHref().orElse(null) + + ", validationDetails=" + validationDetails + + ", dittoHeaders=" + getDittoHeaders() + + ']'; + } + + /** + * A mutable builder with a fluent API for a {@link WotThingModelPayloadValidationException}. + */ + @NotThreadSafe + public static final class Builder extends DittoRuntimeExceptionBuilder { + + private Map> validationDetails; + + private Builder() { + validationDetails = new LinkedHashMap<>(); + message(DEFAULT_MESSAGE); + } + + private Builder(final String validationDescription) { + this(); + description(checkNotNull(validationDescription, "validationDescription")); + } + + private Builder(final Map> validationDetails) { + this(); + this.validationDetails = validationDetails; + } + + public Builder addValidationDetail(final JsonPointer jsonPointer, final List validationErrors) { + validationDetails.put(jsonPointer, validationErrors); + return this; + } + + @Override + protected WotThingModelPayloadValidationException doBuild(final DittoHeaders dittoHeaders, + @Nullable final String message, + @Nullable final String description, + @Nullable final Throwable cause, + @Nullable final URI href) { + return new WotThingModelPayloadValidationException(dittoHeaders, message, description, cause, href, + validationDetails); + } + + } + + /** + * An enumeration of the known {@link org.eclipse.ditto.json.JsonField}s of a {@code WotThingModelPayloadValidationException}. + */ + @Immutable + public static final class JsonFields { + + /** + * JSON field containing the validation details. + */ + static final JsonFieldDefinition VALIDATION_DETAILS = + JsonFactory.newJsonObjectFieldDefinition("validationDetails", FieldType.REGULAR, + JsonSchemaVersion.V_2); + + private JsonFields() { + throw new AssertionError(); + } + + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/WotThingModelValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/WotThingModelValidation.java new file mode 100644 index 0000000000..de2bfd8edf --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/WotThingModelValidation.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import java.util.Map; +import java.util.concurrent.CompletionStage; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; + +/** + * Provides functionality to validate specific parts of a Ditto {@link Thing} and/or Ditto Thing {@link Features} and + * single {@link Feature} instances. + * + * @since 3.6.0 + */ +public interface WotThingModelValidation { + + /** + * Validates the provided {@link Attributes} of a {@code Thing} based on the provided {@code ThingModel}. + * + * @param thingModel the ThingModel to validate against + * @param attributes the attributes to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateThingAttributes(ThingModel thingModel, + @Nullable Attributes attributes, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the provided attribute at {@code attributePointer} having value {@code attributeValue} based on the + * provided {@code ThingModel}. + * + * @param thingModel the ThingModel to validate against + * @param attributePointer the attribute pointer (path) to validate + * @param attributeValue the attribute value to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateThingAttribute(ThingModel thingModel, + JsonPointer attributePointer, + JsonValue attributeValue, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates a deletion inside a thing on "thing" scope, e.g. all attributes, a single attribute or all features. + * + * @param thingModel the ThingModel to validate against + * @param featureThingModels a Map of submodels with their {@code instanceName} as key and their resolved + * {@code ThingModel} as value + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateThingScopedDeletion(ThingModel thingModel, + Map featureThingModels, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the {@code inputPayload} of an inbox message (a WoT action) sent TO a Thing. + * + * @param thingModel the ThingModel to validate against + * @param messageSubject the message subject of the send message + * @param inputPayload the input payload to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateThingActionInput(ThingModel thingModel, + String messageSubject, + @Nullable JsonValue inputPayload, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the {@code outputPayload} of an inbox message response (response to a WoT action) sent TO a Thing. + * + * @param thingModel the ThingModel to validate against + * @param messageSubject the message subject of the send message + * @param outputPayload the output payload to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateThingActionOutput(ThingModel thingModel, + String messageSubject, + @Nullable JsonValue outputPayload, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the {@code dataPayload} of an outbox message (WoT event) sent FROM a Thing. + * + * @param thingModel the ThingModel to validate against + * @param messageSubject the message subject of the send message + * @param dataPayload the output payload to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateThingEventData(ThingModel thingModel, + String messageSubject, + @Nullable JsonValue dataPayload, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the presence of the provided {@link Features} in the provided {@code featureThingModels} Map consisting + * of all submodels of a Thing's {@code ThingModel}. + * + * @param featureThingModels a Map of submodels with their {@code instanceName} as key and their resolved + * {@code ThingModel} as value + * @param features the Features of a Thing to validate presence of the models in + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeaturesPresence(Map featureThingModels, + @Nullable Features features, + ValidationContext context + ); + + /** + * Validates the {@code properties} of the provided {@link Features} against the passed {@code featureThingModels}. + * + * @param featureThingModels a Map of submodels with their {@code instanceName} as key and their resolved + * {@code ThingModel} as value + * @param features the Features of a Thing to validate the {@code properties} in + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeaturesProperties(Map featureThingModels, + @Nullable Features features, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the presence of the provided {@code feature} in the provided {@code featureThingModels} Map consisting + * of all submodels of a Thing's {@code ThingModel}. + * + * @param featureThingModels a Map of submodels with their {@code instanceName} as key and their resolved + * {@code ThingModel} as value + * @param feature the Feature to validate presence of the models in + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeaturePresence(Map featureThingModels, + Feature feature, + ValidationContext context + ); + + /** + * Validates the complete passed {@code feature} (properties and desired properties) based on the provided + * {@code featureThingModel}. + * + * @param featureThingModel the feature's ThingModel to validate against + * @param feature the Feature to validate + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeature(ThingModel featureThingModel, + Feature feature, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the provided {@code featureProperties} (either being properties or desired properties based on the + * passed {@code desiredProperties} flag) against the passed {@code featureThingModel}. + * + * @param featureThingModel the feature's ThingModel to validate against + * @param featureId the feature's id to validate properties in + * @param featureProperties the properties to validate + * @param desiredProperties whether the provided {@code properties} are "desired" properties + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureProperties(ThingModel featureThingModel, + String featureId, + @Nullable FeatureProperties featureProperties, + boolean desiredProperties, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the provided feature property at path {@code propertyPointer} and its value {@code propertyValue} + * against the passed {@code featureThingModel}. + * + * @param featureThingModel the feature's ThingModel to validate against + * @param featureId the feature's id to validate the property in + * @param propertyPointer the feature property pointer (path) to validate + * @param propertyValue the feature property value to validate + * @param desiredProperty whether the provided feature property is a "desired" property + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureProperty(ThingModel featureThingModel, + String featureId, + JsonPointer propertyPointer, + JsonValue propertyValue, + boolean desiredProperty, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates a deletion inside a thing on "feature" scope, e.g. all feature properties or a single feature property. + * + * @param featureThingModels a Map of submodels with their {@code instanceName} as key and their resolved + * {@code ThingModel} as value + * @param featureThingModel the feature's ThingModel to validate against + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureScopedDeletion(Map featureThingModels, + ThingModel featureThingModel, + String featureId, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the {@code inputPayload} of an inbox message (WoT action) sent TO a feature. + * + * @param featureThingModel the ThingModel to validate against + * @param featureId the feature's id to validate the message against + * @param messageSubject the message subject of the send message + * @param inputPayload the input payload to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureActionInput(ThingModel featureThingModel, + String featureId, + String messageSubject, + @Nullable JsonValue inputPayload, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the {@code outputPayload} of an inbox message response (response to a WoT action) sent TO a feature. + * + * @param featureThingModel the ThingModel to validate against + * @param featureId the feature's id to validate the message against + * @param messageSubject the message subject of the send message + * @param outputPayload the output payload to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureActionOutput(ThingModel featureThingModel, + String featureId, + String messageSubject, + @Nullable JsonValue outputPayload, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Validates the {@code dataPayload} of an outbox message (WoT event) sent FROM a feature. + * + * @param featureThingModel the ThingModel to validate against + * @param featureId the feature's id to validate the message against + * @param messageSubject the message subject of the send message + * @param dataPayload the output payload to validate + * @param resourcePath the originating path of the command which caused validation + * @param context the validation context to use, e.g. for dynamic configuration and to access the ditto headers + * @return a CompletionStage finished successfully with {@code null} or finished exceptionally in case of a + * validation error - exceptionally finished with a {@link WotThingModelPayloadValidationException} + */ + CompletionStage validateFeatureEventData(ThingModel featureThingModel, + String featureId, + String messageSubject, + @Nullable JsonValue dataPayload, + JsonPointer resourcePath, + ValidationContext context + ); + + /** + * Creates a new instance of WotThingModelValidation with the given {@code validationConfig}. + * + * @param validationConfig the WoT TM validation config to use. + * @return the created WotThingModelValidation. + */ + static WotThingModelValidation of(final TmValidationConfig validationConfig) { + return new DefaultWotThingModelValidation(validationConfig); + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java new file mode 100644 index 0000000000..353bb14e9b --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 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.wot.validation.config; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.KnownConfigValue; + +/** + * Provides configuration settings for WoT (Web of Things) based validation of Features. + * + * @since 3.6.0 + */ +@Immutable +public interface FeatureValidationConfig { + + /** + * @return whether to enforce/validate a feature whenever its {@code description} is modified. + */ + boolean isEnforceFeatureDescriptionModification(); + + /** + * @return whether to enforce that all modeled features (submodels referenced in the Thing's {@code definition}'s + * WoT model) are present. + */ + boolean isEnforcePresenceOfModeledFeatures(); + + /** + * @return whether to enforce/validate properties of a feature following the defined WoT properties. + */ + boolean isEnforceProperties(); + + /** + * @return whether to enforce/validate desired properties of a feature following the defined WoT properties. + */ + boolean isEnforceDesiredProperties(); + + /** + * @return whether to enforce/validate inbox messages to a feature following the defined WoT action "input". + */ + boolean isEnforceInboxMessagesInput(); + + /** + * @return whether to enforce/validate inbox message responses to a feature following the defined WoT action "output". + */ + boolean isEnforceInboxMessagesOutput(); + + /** + * @return whether to enforce/validate outbox messages from a feature following the defined WoT events. + */ + boolean isEnforceOutboxMessages(); + + /** + * @return whether to forbid deletion of a feature's {@code description}. + */ + boolean isForbidFeatureDescriptionDeletion(); + + /** + * @return whether to forbid adding features to a Thing which were not defined in its {@code definition}'s + * WoT model. + */ + boolean isForbidNonModeledFeatures(); + + /** + * @return whether to forbid persisting properties which are not defined as properties in the WoT model. + */ + boolean isForbidNonModeledProperties(); + + /** + * @return whether to forbid persisting desired properties which are not defined as properties in the WoT model. + */ + boolean isForbidNonModeledDesiredProperties(); + + /** + * @return whether to forbid dispatching of inbox messages which are not defined as actions in the WoT model. + */ + boolean isForbidNonModeledInboxMessages(); + + /** + * @return whether to forbid dispatching of outbox messages which are not defined as events in the WoT model. + */ + boolean isForbidNonModeledOutboxMessages(); + + /** + * An enumeration of the known config path expressions and their associated default values for + * {@code FeatureValidationConfig}. + */ + enum ConfigValue implements KnownConfigValue { + + ENFORCE_FEATURE_DESCRIPTION_MODIFICATION("enforce.feature-description-modification", true), + + ENFORCE_PRESENCE_OF_MODELED_FEATURES("enforce.presence-of-modeled-features", true), + + ENFORCE_PROPERTIES("enforce.properties", true), + + ENFORCE_DESIRED_PROPERTIES("enforce.desired-properties", true), + + ENFORCE_INBOX_MESSAGES_INPUT("enforce.inbox-messages-input", true), + + ENFORCE_INBOX_MESSAGES_OUTPUT("enforce.inbox-messages-output", true), + + ENFORCE_OUTBOX_MESSAGES("enforce.outbox-messages", true), + + FORBID_FEATURE_DESCRIPTION_DELETION("forbid.feature-description-deletion", true), + + FORBID_NON_MODELED_FEATURES("forbid.non-modeled-features", true), + + FORBID_NON_MODELED_PROPERTIES("forbid.non-modeled-properties", true), + + FORBID_NON_MODELED_DESIRED_PROPERTIES("forbid.non-modeled-desired-properties", true), + + FORBID_NON_MODELED_INBOX_MESSAGES("forbid.non-modeled-inbox-messages", true), + + FORBID_NON_MODELED_OUTBOX_MESSAGES("forbid.non-modeled-outbox-messages", true); + + private final String path; + private final Object defaultValue; + + ConfigValue(final String thePath, final Object theDefaultValue) { + path = thePath; + defaultValue = theDefaultValue; + } + + @Override + public Object getDefaultValue() { + return defaultValue; + } + + @Override + public String getConfigPath() { + return path; + } + + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java new file mode 100644 index 0000000000..6eca05c69a --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 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.wot.validation.config; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.KnownConfigValue; + +/** + * Provides configuration settings for WoT (Web of Things) based validation of Things. + * + * @since 3.6.0 + */ +@Immutable +public interface ThingValidationConfig { + + /** + * @return whether to enforce/validate a thing whenever its {@code description} is modified. + */ + boolean isEnforceThingDescriptionModification(); + + /** + * @return whether to enforce/validate attributes of a thing following the defined WoT properties. + */ + boolean isEnforceAttributes(); + + /** + * @return whether to enforce/validate inbox messages to a thing following the defined WoT action "input". + */ + boolean isEnforceInboxMessagesInput(); + + /** + * @return whether to enforce/validate inbox message responses to a thing following the defined WoT action "output". + */ + boolean isEnforceInboxMessagesOutput(); + + /** + * @return whether to enforce/validate outbox messages from a thing following the defined WoT event "data". + */ + boolean isEnforceOutboxMessages(); + + /** + * @return whether to forbid deletion of a thing's {@code description}. + */ + boolean isForbidThingDescriptionDeletion(); + + /** + * @return whether to forbid persisting attributes which are not defined as properties in the WoT model. + */ + boolean isForbidNonModeledAttributes(); + + /** + * @return whether to forbid dispatching of inbox messages which are not defined as actions in the WoT model. + */ + boolean isForbidNonModeledInboxMessages(); + + /** + * @return whether to forbid dispatching of outbox messages which are not defined as events in the WoT model. + */ + boolean isForbidNonModeledOutboxMessages(); + + /** + * An enumeration of the known config path expressions and their associated default values for + * {@code ThingValidationConfig}. + */ + enum ConfigValue implements KnownConfigValue { + + ENFORCE_THING_DESCRIPTION_MODIFICATION("enforce.thing-description-modification", true), + + ENFORCE_ATTRIBUTES("enforce.attributes", true), + + ENFORCE_INBOX_MESSAGES_INPUT("enforce.inbox-messages-input", true), + + ENFORCE_INBOX_MESSAGES_OUTPUT("enforce.inbox-messages-output", true), + + ENFORCE_OUTBOX_MESSAGES("enforce.outbox-messages", true), + + FORBID_THING_DESCRIPTION_DELETION("forbid.thing-description-deletion", true), + + FORBID_NON_MODELED_ATTRIBUTES("forbid.non-modeled-attributes", true), + + FORBID_NON_MODELED_INBOX_MESSAGES("forbid.non-modeled-inbox-messages", true), + + FORBID_NON_MODELED_OUTBOX_MESSAGES("forbid.non-modeled-outbox-messages", true); + + + private final String path; + private final Object defaultValue; + + ConfigValue(final String thePath, final Object theDefaultValue) { + path = thePath; + defaultValue = theDefaultValue; + } + + @Override + public Object getDefaultValue() { + return defaultValue; + } + + @Override + public String getConfigPath() { + return path; + } + + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java new file mode 100644 index 0000000000..a774a5cc6d --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 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.wot.validation.config; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.KnownConfigValue; +import org.eclipse.ditto.wot.validation.ValidationContext; + +/** + * Provides configuration settings for WoT (Web of Things) integration regarding the validation of Things and Features + * based on their WoT ThingModels. + * + * @since 3.6.0 + */ +@Immutable +public interface TmValidationConfig { + + /** + * @return whether the ThingModel validation of Things/Features should be enabled or not. + */ + boolean isEnabled(); + + /** + * @return the config for validating things. + */ + ThingValidationConfig getThingValidationConfig(); + + /** + * @return the config for validating features. + */ + FeatureValidationConfig getFeatureValidationConfig(); + + /** + * Creates a new specific instance of this {@link TmValidationConfig} scoped with the provided validation + * {@code context} of the API call. + * + * @param context the validation context of the API call. + * @return an API call specific instance of the validation config. + */ + TmValidationConfig withValidationContext(@Nullable ValidationContext context); + + /** + * An enumeration of the known config path expressions and their associated default values for + * {@code TmValidationConfig}. + */ + enum ConfigValue implements KnownConfigValue { + + /** + * Whether the TM based validation should be enabled or not. + */ + ENABLED("enabled", true); + + + private final String path; + private final Object defaultValue; + + ConfigValue(final String thePath, final Object theDefaultValue) { + path = thePath; + defaultValue = theDefaultValue; + } + + @Override + public Object getDefaultValue() { + return defaultValue; + } + + @Override + public String getConfigPath() { + return path; + } + + } +} diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/package-info.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/package-info.java new file mode 100644 index 0000000000..78613f6691 --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 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 + */ + +/** + * @since 3.6.0 + */ +@org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault +package org.eclipse.ditto.wot.validation.config; \ No newline at end of file diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/package-info.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/package-info.java new file mode 100644 index 0000000000..e472596f25 --- /dev/null +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 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 + */ + +/** + * @since 3.6.0 + */ +@org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault +package org.eclipse.ditto.wot.validation; \ No newline at end of file diff --git a/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java new file mode 100644 index 0000000000..0ba8df3989 --- /dev/null +++ b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java @@ -0,0 +1,933 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.wot.model.Action; +import org.eclipse.ditto.wot.model.Actions; +import org.eclipse.ditto.wot.model.AtContext; +import org.eclipse.ditto.wot.model.BaseLink; +import org.eclipse.ditto.wot.model.Event; +import org.eclipse.ditto.wot.model.Events; +import org.eclipse.ditto.wot.model.Links; +import org.eclipse.ditto.wot.model.Properties; +import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.SingleAtContext; +import org.eclipse.ditto.wot.model.SingleDataSchema; +import org.eclipse.ditto.wot.model.SingleUriAtContext; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.TmOptional; +import org.eclipse.ditto.wot.model.TmOptionalElement; +import org.eclipse.ditto.wot.validation.config.FeatureValidationConfig; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Provides unit tests for testing the "feature" related functionality of {@link WotThingModelValidation}. + * + * TODO TJ enhance with tests on deeper levels + */ +public final class WotThingModelValidationFeatureLevelTest { + + private static final String DITTO_CONTEXT_PREFIX = "ditto"; + private static final String CATEGORY_CONFIG = "config"; + + private static final String PROP_SOME_BOOL = "someBool"; + private static final String PROP_SOME_INT = "someInt"; + private static final JsonPointer PROP_PATH_SOME_INT = JsonPointer.of(CATEGORY_CONFIG + "/" + PROP_SOME_INT); + private static final String PROP_SOME_NUMBER = "someNumber"; + private static final String PROP_SOME_STRING = "someString"; + private static final JsonPointer PROP_PATH_SOME_STRING = JsonPointer.of(CATEGORY_CONFIG + "/" + PROP_SOME_STRING); + private static final String PROP_SOME_ARRAY_STRINGS = "someArray_strings"; + private static final String PROP_SOME_OBJECT = "someObject"; + private static final String PROP_NESTED_OBJECT = "someNestedObject"; + private static final String PROP_NESTED_OBJECT_BOOL = "someNestedObjectBool"; + private static final String PROP_NESTED_OBJECT_STRING = "someNestedObjectString"; + + private static final JsonObject PROP_KNOWN_SOME_OBJECT = JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 3) + .set(PROP_SOME_STRING, "helo") + .build(); + + private static final JsonObject PROP_KNOWN_SOME_NESTED_OBJECT = JsonObject.newBuilder() + .set(PROP_NESTED_OBJECT_BOOL, true) + .set(PROP_NESTED_OBJECT_STRING, "woha, nested!") + .build(); + + private static final Properties KNOWN_PROPERTIES = Properties.from(List.of( + Property.newBuilder(PROP_SOME_BOOL) + .setSchema(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_INT) + .setSchema(SingleDataSchema.newIntegerSchemaBuilder().build()) + .set(DITTO_CONTEXT_PREFIX + ":category", CATEGORY_CONFIG) + .build(), + Property.newBuilder(PROP_SOME_NUMBER) + .setSchema(SingleDataSchema.newNumberSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_STRING) + .setSchema(SingleDataSchema.newStringSchemaBuilder().build()) + .set(DITTO_CONTEXT_PREFIX + ":category", CATEGORY_CONFIG) + .build(), + Property.newBuilder(PROP_SOME_ARRAY_STRINGS) + .setSchema(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newStringSchemaBuilder().build()) + .build()) + .build(), + Property.newBuilder(PROP_SOME_OBJECT) + .setSchema(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build(), + PROP_NESTED_OBJECT, SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_NESTED_OBJECT_BOOL, + SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_NESTED_OBJECT_STRING, + SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_NESTED_OBJECT_STRING)) + .build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .enhanceObjectBuilder(builder -> builder.set("additionalProperties", false)) + .build()) + .build() + )); + + private static final String ACTION_PROCESS_BOOL = "processBool"; + private static final String ACTION_PROCESS_OBJECT = "processObject"; + + private static final Actions KNOWN_ACTIONS = Actions.from(List.of( + Action.newBuilder(ACTION_PROCESS_BOOL) + .setInput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .setOutput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Action.newBuilder(ACTION_PROCESS_OBJECT) + .setInput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build() + ) + .setOutput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + )); + + private static final String EVENT_EMIT_INT = "emitInt"; + private static final String EVENT_EMIT_ARRAY = "emitArray_objects"; + + private static final Events KNOWN_EVENTS = Events.from(List.of( + Event.newBuilder(EVENT_EMIT_INT) + .setData(SingleDataSchema.newIntegerSchemaBuilder().build()) + .build(), + Event.newBuilder(EVENT_EMIT_ARRAY) + .setData(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + ) + .build() + )); + + private static final String KNOWN_FEATURE_ID = "known-feature"; + private static final String KNOWN_FEATURE_ID_2 = "known-feature-2"; + + private static final ThingModel KNOWN_THING_LEVEL_TM_WITH_SUBMODELS = ThingModel.newBuilder() + .setLinks(Links.of(List.of( + BaseLink.newLinkBuilder() + .setType("tm:submodel") + .build() + ))) + .build(); + + private static final ThingModel KNOWN_FEATURE_LEVEL_TM = ThingModel.newBuilder() + .setAtContext(AtContext.newMultipleAtContext(List.of( + SingleAtContext.newSinglePrefixedAtContext(DITTO_CONTEXT_PREFIX, + SingleUriAtContext.DITTO_WOT_EXTENSION) + ))) + .setProperties(KNOWN_PROPERTIES) + .setTmOptional(TmOptional.of(List.of( + TmOptionalElement.of("/properties/" + PROP_SOME_ARRAY_STRINGS), + TmOptionalElement.of("/properties/" + PROP_SOME_OBJECT) + ))) + .setActions(KNOWN_ACTIONS) + .setEvents(KNOWN_EVENTS) + .build(); + + private static final FeatureProperties KNOWN_FEATURE_PROPERTIES = FeatureProperties.newBuilder() + .set(PROP_SOME_BOOL, true) + .set(PROP_PATH_SOME_INT, 42) + .set(PROP_SOME_NUMBER, 42.23) + .set(PROP_PATH_SOME_STRING, "some") + .build(); + + + private WotThingModelValidation sut; + + @Before + public void setUp() { + final TmValidationConfig validationConfig = mock(TmValidationConfig.class); + when(validationConfig.isEnabled()).thenReturn(true); + + final FeatureValidationConfig featureValidationConfig = mock(FeatureValidationConfig.class); + when(featureValidationConfig.isEnforceFeatureDescriptionModification()).thenReturn(true); + when(featureValidationConfig.isEnforcePresenceOfModeledFeatures()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledFeatures()).thenReturn(true); + when(featureValidationConfig.isEnforceProperties()).thenReturn(true); + when(featureValidationConfig.isEnforceDesiredProperties()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledProperties()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledDesiredProperties()).thenReturn(true); + when(featureValidationConfig.isEnforceInboxMessagesInput()).thenReturn(true); + when(featureValidationConfig.isEnforceInboxMessagesOutput()).thenReturn(true); + when(featureValidationConfig.isEnforceOutboxMessages()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledInboxMessages()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledOutboxMessages()).thenReturn(true); + when(validationConfig.getFeatureValidationConfig()).thenReturn(featureValidationConfig); + + sut = WotThingModelValidation.of(validationConfig); + } + + @Test + public void validateFeaturesDeletionSucceedsWithThingLevelModelNotHavingSubmodels() { + internalCheckFail(false, sut.validateThingScopedDeletion(ThingModel.newBuilder().build(), + Map.of(), + JsonPointer.of("features"), + provideValidationContext() + )); + } + + @Test + public void validateFeaturesDeletionFailsWithThingLevelModelHavingSubmodels() { + internalCheckFail(true, sut.validateThingScopedDeletion(KNOWN_THING_LEVEL_TM_WITH_SUBMODELS, + Map.of(KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM), + JsonPointer.of("features"), + provideValidationContext() + )); + } + + @Test + public void validateFeaturesPresenceSucceedsWhenAllModeledFeaturesArePresent() { + checkValidateFeaturesPresence(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + false + ); + } + + @Test + public void validateFeaturesPresenceFailsWhenNotAllModeledFeaturesArePresent() { + checkValidateFeaturesPresence(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturesPresenceFailsWhenNonModeledFeaturesAreProvided() { + checkValidateFeaturesPresence(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId("unknown-feature") + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturesPropertiesSucceeds() { + checkValidateFeaturesProperties(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + false + ); + } + + @Test + public void validateFeaturesPropertiesFailsWhenMissingRequiredProperty() { + checkValidateFeaturesProperties(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().remove(PROP_SOME_BOOL).build()) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturesPropertiesFailsWhenPropertyHasWrongDatatype() { + checkValidateFeaturesProperties(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().set(PROP_SOME_BOOL, "not a bool").build()) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturePresenceSucceeds() { + checkValidateFeaturePresence(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build(), + false + ); + } + + @Test + public void validateFeaturePresenceFails() { + checkValidateFeaturePresence(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId("unknown-id") + .build(), + true + ); + } + + @Test + public void validateFeatureSucceeds() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build(), + false + ); + } + + @Test + public void validateFeatureSucceedsWithOptionalDesiredPropertyPresent() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .desiredProperties(FeatureProperties.newBuilder() + .set(PROP_PATH_SOME_INT, 42) + .build() + ) + .withId(KNOWN_FEATURE_ID) + .build(), + false + ); + } + + @Test + public void validateFeatureFailsWhenMissingRequiredProperty() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().remove(PROP_PATH_SOME_STRING).build()) + .withId(KNOWN_FEATURE_ID) + .build(), + true + ); + } + + @Test + public void validateFeatureFailsWithOptionalDesiredPropertyHavingWrongDatatype() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .desiredProperties(FeatureProperties.newBuilder() + .set(PROP_PATH_SOME_INT, "not an int") + .build() + ) + .withId(KNOWN_FEATURE_ID) + .build(), + true + ); + } + + @Test + public void validateFeaturePropertySucceedsForBooleanProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_BOOL, false, JsonValue.of(true), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForBooleanProperty() { + checkValidateFeatureProperties(PROP_SOME_BOOL, false, JsonValue.of(true), false); + } + + @Test + public void validateFeaturePropertyFailsForBooleanProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_BOOL, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertiesFailsForBooleanProperty() { + checkValidateFeatureProperties(PROP_SOME_BOOL, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertySucceedsForIntegerProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_INT, false, JsonValue.of(42), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForIntegerProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_INT, false, JsonValue.of(42), false); + } + + @Test + public void validateFeaturePropertyFailsForIntegerProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_INT, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertiesFailsForIntegerProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_INT, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertySucceedsForNumberProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_NUMBER, false, JsonValue.of(42.23), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForNumberProperty() { + checkValidateFeatureProperties(PROP_SOME_NUMBER, false, JsonValue.of(42.23), false); + } + + @Test + public void validateFeaturePropertyFailsForNumberProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_NUMBER, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertiesFailsForNumberProperty() { + checkValidateFeatureProperties(PROP_SOME_NUMBER, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertySucceedsForStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_STRING, false, JsonValue.of("some"), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForStringProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_STRING, false, JsonValue.of("some"), false); + } + + @Test + public void validateFeaturePropertyFailsForStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_STRING, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertiesFailsForStringProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_STRING, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertySucceedsForArraysStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, + JsonArray.of("some", "string", "arr"), + false); + } + + @Test + public void validateFeaturePropertiesSucceedsForArraysStringProperty() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, JsonArray.of("some", "string", "arr"), false); + } + + @Test + public void validateFeaturePropertyFailsForArraysStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, JsonArray.of(false, true, false), + true); + } + + @Test + public void validateFeaturePropertiesFailsForArraysStringProperty() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, JsonArray.of(false, true, false), true); + } + + @Test + public void validateFeaturePropertySucceedsForObjectProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_OBJECT, false, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateFeaturePropertySucceedsForCategoryUpdateContainingAllRequiredProperties() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonObject.newBuilder() + .set(PROP_SOME_INT, 44) + .set(PROP_SOME_STRING, "some") + .build(), + false + ); + } + + @Test + public void validateFeaturePropertyFailsForCategoryUpdateMissingRequiredProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonObject.newBuilder() + .set(PROP_SOME_INT, 44) + .build(), + true + ); + } + + @Test + public void validateFeaturePropertyFailsForCategoryUpdateNotBeingObject() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonValue.of("not a category object"), + true + ); + } + + @Test + public void validateFeaturePropertiesSucceedsForObjectProperty() { + checkValidateFeatureProperties(PROP_SOME_OBJECT, false, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateFeaturePropertyFailsForObjectProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_OBJECT, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertiesSucceedsForNestedObjectProperty() { + checkValidateFeatureProperty( + KNOWN_FEATURE_ID, + JsonPointer.of(PROP_SOME_OBJECT + "/" + PROP_NESTED_OBJECT), + false, + PROP_KNOWN_SOME_NESTED_OBJECT, + false + ); + } + + @Test + public void validateFeaturePropertiesFailsForNestedObjectPropertyWithWrongSubfieldType() { + checkValidateFeatureProperty( + KNOWN_FEATURE_ID, + JsonPointer.of(PROP_SOME_OBJECT + "/" + PROP_NESTED_OBJECT), + false, + PROP_KNOWN_SOME_NESTED_OBJECT.toBuilder() + .set(PROP_NESTED_OBJECT_BOOL, 42) + .build(), + true + ); + } + + @Test + public void validateFeaturePropertiesSucceedsForNestedStringInObjectProperty() { + checkValidateFeatureProperty( + KNOWN_FEATURE_ID, + JsonPointer.of(PROP_SOME_OBJECT + "/" + PROP_NESTED_OBJECT + "/" + PROP_NESTED_OBJECT_STRING), + false, + JsonValue.of("succeeds!"), + false + ); + } + + @Test + public void validateFeaturePropertiesFailsForNestedStringInObjectPropertyWithWrongSubfieldType() { + checkValidateFeatureProperty( + KNOWN_FEATURE_ID, + JsonPointer.of(PROP_SOME_OBJECT + "/" + PROP_NESTED_OBJECT + "/" + PROP_NESTED_OBJECT_BOOL), + false, + JsonValue.of("what the heck!"), + true + ); + } + + @Test + public void validateFeaturePropertiesFailsForObjectProperty() { + checkValidateFeatureProperties(PROP_SOME_OBJECT, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertyFailsForObjectAttributeMissingRequiredField() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_PATH_SOME_STRING).build(), true); + } + + @Test + public void validateFeaturePropertiesFailsForObjectAttributeMissingRequiredField() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_PATH_SOME_STRING).build(), true); + } + + @Test + public void validateFeaturePropertyFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateFeaturePropertiesFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateFeaturePropertyFailsForNonModeledProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, "newStuff", false, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertiesFailsForNonModeledProperties() { + checkValidateFeatureProperties("newStuff", false, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertyFailsForNonModeledDesiredProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, "newStuff", true, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertiesFailsForNonModeledDesiredProperties() { + checkValidateFeatureProperties("newStuff", true, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertyDeletionSucceedsForOptionalArrayProperty() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/" + PROP_SOME_ARRAY_STRINGS), + false + ); + } + + @Test + public void validateFeaturePropertyDeletionFailsForFeatureContainingRequiredProperties() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID), + true + ); + } + + @Test + public void validateFeaturePropertyDeletionFailsForFeaturePropertiesContainingRequiredProperties() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/"), + true + ); + } + + @Test + public void validateFeaturePropertyDeletionFailsForFeaturePropertiesCategoryContainingRequiredProperties() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/" + CATEGORY_CONFIG), + true + ); + } + + @Test + public void validateFeatureActionBoolInputSucceeds() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateFeatureActionBoolInputFailsWrongDatatype() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateFeatureActionObjectInputSucceeds() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateFeatureActionObjectInputFailsWrongDatatype() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateFeatureActionObjectInputFailsMissingRequiredField() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_PATH_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateNonModeledFeatureActionInputFails() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, "someNewAction", JsonValue.of(true), true); + } + + @Test + public void validateFeatureActionBoolOutputSucceeds() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateFeatureActionBoolOutputFailsWrongDatatype() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateFeatureActionObjectOutputSucceeds() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateFeatureActionObjectOutputFailsWrongDatatype() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateFeatureActionObjectOutputFailsMissingRequiredField() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_PATH_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateFeatureEventIntDataSucceeds() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_INT, JsonValue.of(33), false); + } + + @Test + public void validateFeatureEventBoolDataFailsWrongDatatype() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_INT, JsonValue.of("oh no"), true); + } + + @Test + public void validateFeatureEventArrayDataSucceeds() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build() + ).build(), false); + } + + @Test + public void validateFeatureEventArrayDataFailsWrongDatatype() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonObject.empty(), true); + } + + @Test + public void validateFeatureEventArrayDataFailsMissingRequiredFieldInsideObject() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_PATH_SOME_INT, 23) + .set(PROP_PATH_SOME_STRING, "yes!") + .build() + ).build(), true); + } + + @Test + public void validateNonModeledFeatureEventDataFails() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, "someNewEvent", JsonValue.of(true), true); + } + + private void checkValidateFeaturesPresence(@Nullable final Features features, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeaturesPresence(Map.of( + KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM + ), + features, + provideValidationContext() + )); + } + + private void checkValidateFeaturesProperties(@Nullable final Features features, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeaturesProperties(Map.of( + KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM + ), + features, + JsonPointer.of("features"), + provideValidationContext() + )); + } + + private void checkValidateFeaturePresence(final Feature feature, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeaturePresence(Map.of( + KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM + ), + feature, + provideValidationContext() + )); + } + + private void checkValidateFeature(final Feature feature, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeature(KNOWN_FEATURE_LEVEL_TM, + feature, + JsonPointer.of("features/" + feature.getId()), + provideValidationContext() + )); + } + + private void checkValidateFeatureProperty(final String featureId, final CharSequence propertyPath, + final boolean desiredProperty, + final JsonValue propertyValue, + final boolean mustFail + ) { + final JsonPointer propertyPointer = JsonPointer.of(propertyPath); + internalCheckFail(mustFail, sut.validateFeatureProperty(KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID, + propertyPointer, + propertyValue, + desiredProperty, + JsonPointer.of("features/" + featureId + "/properties").append(propertyPointer), + provideValidationContext() + )); + } + + private void checkValidateFeatureProperties(final CharSequence propertyPath, final boolean desiredProperties, + final JsonValue propertyValue, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureProperties(KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID, + KNOWN_FEATURE_PROPERTIES.toBuilder() + .set(propertyPath, propertyValue) + .build(), + desiredProperties, + JsonPointer.of("attributes"), + provideValidationContext() + )); + } + + private void checkValidateFeaturePropertyDeletion(final JsonPointer resourcePath, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeatureScopedDeletion(Map.of(KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM), + KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID, + resourcePath, + provideValidationContext() + )); + } + + private void checkValidateFeatureActionInput(final String featureId, final String actionName, + @Nullable final JsonValue inputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureActionInput(KNOWN_FEATURE_LEVEL_TM, + featureId, + actionName, + inputData, + JsonPointer.of("features/" + featureId + "/inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateFeatureActionOutput(final String featureId, final String actionName, + @Nullable final JsonValue outputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureActionOutput(KNOWN_FEATURE_LEVEL_TM, + featureId, + actionName, + outputData, + JsonPointer.of("features/" + featureId + "/inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateFeatureEventData(final String featureId, final String eventName, + @Nullable final JsonValue data, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureEventData(KNOWN_FEATURE_LEVEL_TM, + featureId, + eventName, + data, + JsonPointer.of("features/" + featureId + "/outbox/messages/" + eventName), + provideValidationContext() + )); + } + + private static ValidationContext provideValidationContext() { + return ValidationContext.buildValidationContext(DittoHeaders.empty(), null, null); + } + + private static void internalCheckFail(final boolean mustFail, final CompletionStage stage) { + if (mustFail) { + assertThat(stage) + .isCompletedExceptionally() + .failsWithin(50, TimeUnit.MILLISECONDS) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(WotThingModelPayloadValidationException.class); + } else { + assertThat(stage).isNotCompletedExceptionally().isDone(); + } + } +} \ No newline at end of file diff --git a/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java new file mode 100644 index 0000000000..e5c80929f2 --- /dev/null +++ b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.wot.model.Action; +import org.eclipse.ditto.wot.model.Actions; +import org.eclipse.ditto.wot.model.AtContext; +import org.eclipse.ditto.wot.model.Event; +import org.eclipse.ditto.wot.model.Events; +import org.eclipse.ditto.wot.model.Properties; +import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.SingleDataSchema; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.TmOptional; +import org.eclipse.ditto.wot.model.TmOptionalElement; +import org.eclipse.ditto.wot.validation.config.ThingValidationConfig; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Provides unit tests for testing the "thing" related functionality of {@link WotThingModelValidation}. + */ +public final class WotThingModelValidationThingLevelTest { + + private static final String PROP_SOME_BOOL = "someBool"; + private static final String PROP_SOME_INT = "someInt"; + private static final String PROP_SOME_NUMBER = "someNumber"; + private static final String PROP_SOME_STRING = "someString"; + private static final String PROP_SOME_ARRAY_STRINGS = "someArray_strings"; + private static final String PROP_SOME_OBJECT = "someObject"; + + private static final JsonObject PROP_KNOWN_SOME_OBJECT = JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 3) + .set(PROP_SOME_STRING, "helo") + .build(); + + private static final Properties KNOWN_PROPERTIES = Properties.from(List.of( + Property.newBuilder(PROP_SOME_BOOL) + .setSchema(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_INT) + .setSchema(SingleDataSchema.newIntegerSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_NUMBER) + .setSchema(SingleDataSchema.newNumberSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_STRING) + .setSchema(SingleDataSchema.newStringSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_ARRAY_STRINGS) + .setSchema(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newStringSchemaBuilder().build()) + .build()) + .build(), + Property.newBuilder(PROP_SOME_OBJECT) + .setSchema(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .enhanceObjectBuilder(builder -> builder.set("additionalProperties", false)) + .build()) + .build() + )); + + private static final String ACTION_PROCESS_BOOL = "processBool"; + private static final String ACTION_PROCESS_OBJECT = "processObject"; + + private static final Actions KNOWN_ACTIONS = Actions.from(List.of( + Action.newBuilder(ACTION_PROCESS_BOOL) + .setInput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .setOutput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Action.newBuilder(ACTION_PROCESS_OBJECT) + .setInput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build() + ) + .setOutput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + )); + + private static final String EVENT_EMIT_INT = "emitInt"; + private static final String EVENT_EMIT_ARRAY = "emitArray_objects"; + + private static final Events KNOWN_EVENTS = Events.from(List.of( + Event.newBuilder(EVENT_EMIT_INT) + .setData(SingleDataSchema.newIntegerSchemaBuilder().build()) + .build(), + Event.newBuilder(EVENT_EMIT_ARRAY) + .setData(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + ) + .build() + )); + + private static final ThingModel KNOWN_THING_LEVEL_TM = ThingModel.newBuilder() + .setAtContext(AtContext.newSingleUriAtContext("foo")) + .setProperties(KNOWN_PROPERTIES) + .setTmOptional(TmOptional.of(List.of( + TmOptionalElement.of("/properties/" + PROP_SOME_ARRAY_STRINGS), + TmOptionalElement.of("/properties/" + PROP_SOME_OBJECT) + ))) + .setActions(KNOWN_ACTIONS) + .setEvents(KNOWN_EVENTS) + .build(); + + private static final Attributes KNOWN_THING_ATTRIBUTES = Attributes.newBuilder() + .set(PROP_SOME_BOOL, true) + .set(PROP_SOME_INT, 42) + .set(PROP_SOME_NUMBER, 42.23) + .set(PROP_SOME_STRING, "some") + .build(); + + + private WotThingModelValidation sut; + + @Before + public void setUp() { + final TmValidationConfig validationConfig = mock(TmValidationConfig.class); + when(validationConfig.isEnabled()).thenReturn(true); + + final ThingValidationConfig thingValidationConfig = mock(ThingValidationConfig.class); + when(thingValidationConfig.isEnforceThingDescriptionModification()).thenReturn(true); + when(thingValidationConfig.isEnforceAttributes()).thenReturn(true); + when(thingValidationConfig.isForbidNonModeledAttributes()).thenReturn(true); + when(thingValidationConfig.isEnforceInboxMessagesInput()).thenReturn(true); + when(thingValidationConfig.isEnforceInboxMessagesOutput()).thenReturn(true); + when(thingValidationConfig.isEnforceOutboxMessages()).thenReturn(true); + when(thingValidationConfig.isForbidNonModeledInboxMessages()).thenReturn(true); + when(thingValidationConfig.isForbidNonModeledOutboxMessages()).thenReturn(true); + when(validationConfig.getThingValidationConfig()).thenReturn(thingValidationConfig); + + sut = WotThingModelValidation.of(validationConfig); + } + + @Test + public void validateThingAttributeSucceedsForBooleanAttribute() { + checkValidateThingAttribute(PROP_SOME_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingAttributesSucceedsForBooleanAttribute() { + checkValidateThingAttributes(PROP_SOME_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingAttributeFailsForBooleanAttribute() { + checkValidateThingAttribute(PROP_SOME_BOOL, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributesFailsForBooleanAttribute() { + checkValidateThingAttributes(PROP_SOME_BOOL, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributeSucceedsForIntegerAttribute() { + checkValidateThingAttribute(PROP_SOME_INT, JsonValue.of(42), false); + } + + @Test + public void validateThingAttributesSucceedsForIntegerAttribute() { + checkValidateThingAttributes(PROP_SOME_INT, JsonValue.of(42), false); + } + + @Test + public void validateThingAttributeFailsForIntegerAttribute() { + checkValidateThingAttribute(PROP_SOME_INT, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributesFailsForIntegerAttribute() { + checkValidateThingAttributes(PROP_SOME_INT, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributeSucceedsForNumberAttribute() { + checkValidateThingAttribute(PROP_SOME_NUMBER, JsonValue.of(42.23), false); + } + + @Test + public void validateThingAttributesSucceedsForNumberAttribute() { + checkValidateThingAttributes(PROP_SOME_NUMBER, JsonValue.of(42.23), false); + } + + @Test + public void validateThingAttributeFailsForNumberAttribute() { + checkValidateThingAttribute(PROP_SOME_NUMBER, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributesFailsForNumberAttribute() { + checkValidateThingAttributes(PROP_SOME_NUMBER, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributeSucceedsForStringAttribute() { + checkValidateThingAttribute(PROP_SOME_STRING, JsonValue.of("some"), false); + } + + @Test + public void validateThingAttributesSucceedsForStringAttribute() { + checkValidateThingAttributes(PROP_SOME_STRING, JsonValue.of("some"), false); + } + + @Test + public void validateThingAttributeFailsForStringAttribute() { + checkValidateThingAttribute(PROP_SOME_STRING, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributesFailsForStringAttribute() { + checkValidateThingAttributes(PROP_SOME_STRING, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributeSucceedsForArraysStringAttribute() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, JsonArray.of("some", "string", "arr"), false); + } + + @Test + public void validateThingAttributesSucceedsForArraysStringAttribute() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, JsonArray.of("some", "string", "arr"), false); + } + + @Test + public void validateThingAttributeFailsForArraysStringAttribute() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, JsonArray.of(false, true, false), true); + } + + @Test + public void validateThingAttributesFailsForArraysStringAttribute() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, JsonArray.of(false, true, false), true); + } + + @Test + public void validateThingAttributeSucceedsForObjectAttribute() { + checkValidateThingAttribute(PROP_SOME_OBJECT, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateThingAttributesSucceedsForObjectAttribute() { + checkValidateThingAttributes(PROP_SOME_OBJECT, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateThingAttributeFailsForObjectAttribute() { + checkValidateThingAttribute(PROP_SOME_OBJECT, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributesFailsForObjectAttribute() { + checkValidateThingAttributes(PROP_SOME_OBJECT, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributeFailsForObjectAttributeMissingRequiredField() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_SOME_STRING).build(), true); + } + + @Test + public void validateThingAttributesFailsForObjectAttributeMissingRequiredField() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_SOME_STRING).build(), true); + } + + @Test + public void validateThingAttributeFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateThingAttributesFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateThingAttributeFailsForNonModeledAttribute() { + checkValidateThingAttribute("newStuff", JsonValue.of(true), true); + } + + @Test + public void validateThingAttributesFailsForNonModeledAttribute() { + checkValidateThingAttributes("newStuff", JsonValue.of(true), true); + } + + @Test + public void validateThingAttributeDeletionFailsForRequiredBooleanAttribute() { + checkValidateThingAttributeDeletion(PROP_SOME_BOOL, true); + } + + @Test + public void validateThingAttributeDeletionSucceedsForOptionalArrayAttribute() { + checkValidateThingAttributeDeletion(PROP_SOME_ARRAY_STRINGS, false); + } + + @Test + public void validateThingActionBoolInputSucceeds() { + checkValidateThingActionInput(ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingActionBoolInputFailsWrongDatatype() { + checkValidateThingActionInput(ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateThingActionObjectInputSucceeds() { + checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateThingActionObjectInputFailsWrongDatatype() { + checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateThingActionObjectInputFailsMissingRequiredField() { + checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateNonModeledThingActionInputFails() { + checkValidateThingActionInput("someNewAction", JsonValue.of(true), true); + } + + @Test + public void validateThingActionBoolOutputSucceeds() { + checkValidateThingActionOutput(ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingActionBoolOutputFailsWrongDatatype() { + checkValidateThingActionOutput(ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateThingActionObjectOutputSucceeds() { + checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateThingActionObjectOutputFailsWrongDatatype() { + checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateThingActionObjectOutputFailsMissingRequiredField() { + checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateThingEventIntDataSucceeds() { + checkValidateThingEventData(EVENT_EMIT_INT, JsonValue.of(33), false); + } + + @Test + public void validateThingEventBoolDataFailsWrongDatatype() { + checkValidateThingEventData(EVENT_EMIT_INT, JsonValue.of("oh no"), true); + } + + @Test + public void validateThingEventArrayDataSucceeds() { + checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build() + ).build(), false); + } + + @Test + public void validateThingEventArrayDataFailsWrongDatatype() { + checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonObject.empty(), true); + } + + @Test + public void validateThingEventArrayDataFailsMissingRequiredFieldInsideObject() { + checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build() + ).build(), true); + } + + @Test + public void validateNonModeledThingEventDataFails() { + checkValidateThingEventData("someNewEvent", JsonValue.of(true), true); + } + + private void checkValidateThingAttribute(final CharSequence attributePath, final JsonValue attributeValue, + final boolean mustFail + ) { + final JsonPointer attributePointer = JsonPointer.of(attributePath); + internalCheckFail(mustFail, sut.validateThingAttribute(KNOWN_THING_LEVEL_TM, + attributePointer, + attributeValue, + JsonPointer.of("attributes").append(attributePointer), + provideValidationContext() + )); + } + + private void checkValidateThingAttributes(final CharSequence attributePath, final JsonValue attributeValue, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingAttributes(KNOWN_THING_LEVEL_TM, + KNOWN_THING_ATTRIBUTES.toBuilder() + .set(attributePath, attributeValue) + .build(), + JsonPointer.of("attributes"), + provideValidationContext() + )); + } + + private void checkValidateThingAttributeDeletion(final CharSequence attributePath, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateThingScopedDeletion(KNOWN_THING_LEVEL_TM, + Map.of(), + JsonPointer.of("attributes/" + attributePath), + provideValidationContext() + )); + } + + private void checkValidateThingActionInput(final String actionName, @Nullable final JsonValue inputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingActionInput(KNOWN_THING_LEVEL_TM, + actionName, + inputData, + JsonPointer.of("inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateThingActionOutput(final String actionName, @Nullable final JsonValue outputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingActionOutput(KNOWN_THING_LEVEL_TM, + actionName, + outputData, + JsonPointer.of("inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateThingEventData(final String eventName, @Nullable final JsonValue data, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingEventData(KNOWN_THING_LEVEL_TM, + eventName, + data, + JsonPointer.of("outbox/messages/" + eventName), + provideValidationContext() + )); + } + + private static ValidationContext provideValidationContext() { + return ValidationContext.buildValidationContext(DittoHeaders.empty(), null, null); + } + + private static void internalCheckFail(final boolean mustFail, final CompletionStage stage) { + if (mustFail) { + assertThat(stage) + .isCompletedExceptionally() + .failsWithin(50, TimeUnit.MILLISECONDS) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(WotThingModelPayloadValidationException.class); + } else { + assertThat(stage).isNotCompletedExceptionally().isDone(); + } + } +} \ No newline at end of file