diff --git a/edge/service/src/main/resources/ditto-edge-service.conf b/edge/service/src/main/resources/ditto-edge-service.conf index c191c806ef..e3f55e45bb 100644 --- a/edge/service/src/main/resources/ditto-edge-service.conf +++ b/edge/service/src/main/resources/ditto-edge-service.conf @@ -33,7 +33,7 @@ ditto { ask-with-retry { # maximum duration to wait for answers from entity shard regions - ask-timeout = 3s + ask-timeout = 5s ask-timeout = ${?CONCIERGE_CACHES_ASK_TIMEOUT} # one of: OFF, NO_DELAY, FIXED_DELAY, BACKOFF_DELAY diff --git a/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/Cache.java b/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/Cache.java index f7c72e7091..4da7837983 100644 --- a/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/Cache.java +++ b/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/Cache.java @@ -36,6 +36,18 @@ public interface Cache { */ CompletableFuture> get(K key); + /** + * Returns a {@link CompletableFuture} returning the value which is associated with the specified key, specifying + * an {@code errorHandler}. + * + * @param key the key to get the associated value for. + * @param errorHandler function to invoke when a {@code Throwable} is encountered by the cache loader. + * @return a {@link CompletableFuture} returning the value which is associated with the specified key or an empty + * {@link Optional}. + * @throws NullPointerException if {@code key} is {@code null}. + */ + CompletableFuture> get(K key, Function> errorHandler); + /** * Retrieve the value associated with a key in a future if it exists in the cache, or a future empty optional if * it does not. The cache loader will never be called. diff --git a/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/CaffeineCache.java b/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/CaffeineCache.java index 58286f7874..c48f638bfa 100644 --- a/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/CaffeineCache.java +++ b/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/CaffeineCache.java @@ -17,9 +17,11 @@ import java.util.Collection; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.function.BiFunction; +import java.util.function.Function; import javax.annotation.Nullable; @@ -165,6 +167,20 @@ public CompletableFuture> get(final K key) { return asyncCache.get(key, asyncLoad).thenApply(Optional::ofNullable); } + @Override + public CompletableFuture> get(final K key, final Function> errorHandler) { + requireNonNull(key); + + return asyncCache.get(key, asyncLoad).thenApply(Optional::ofNullable) + .exceptionally(throwable -> { + if (throwable instanceof CompletionException completionException) { + return errorHandler.apply(completionException.getCause()); + } else { + return errorHandler.apply(throwable); + } + }); + } + /** * Lookup a value in cache, or create it via {@code mappingFunction} and store it if the value was not cached. * Only available for Caffeine caches. diff --git a/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/ProjectedCache.java b/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/ProjectedCache.java index 6291456189..84628459b5 100644 --- a/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/ProjectedCache.java +++ b/internal/utils/cache/src/main/java/org/eclipse/ditto/internal/utils/cache/ProjectedCache.java @@ -48,6 +48,11 @@ public CompletableFuture> get(final K key) { return cache.get(key).thenApply(projectOptional); } + @Override + public CompletableFuture> get(final K key, final Function> errorHandler) { + return cache.get(key, throwable -> errorHandler.apply(throwable).map(embed)).thenApply(projectOptional); + } + @Override public CompletableFuture> getIfPresent(final K key) { return cache.getIfPresent(key).thenApply(projectOptional); 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 680027594c..78074ff45b 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 @@ -565,12 +565,20 @@ private record PersistEventAsync< /** * Persist an event, modify actor state by the event strategy, then invoke the handler. * - * @param event the event to persist and apply. + * @param eventStage the event stage to persist and apply. * @param handler what happens afterwards. + * @param errorHandler the errorHandler to invoke for encountered throwables from the CompletionStage */ - protected void persistAndApplyEventAsync(final CompletionStage event, final BiConsumer handler) { - Patterns.pipe(event.thenApply(e -> new PersistEventAsync<>(e, handler)), getContext().getDispatcher()) - .to(getSelf()); + protected void persistAndApplyEventAsync(final CompletionStage eventStage, final BiConsumer handler, + final Consumer errorHandler) { + Patterns.pipe(eventStage.handle((e, throwable) -> { + if (throwable != null) { + errorHandler.accept(throwable); + return null; + } else { + return new PersistEventAsync<>(e, handler); + } + }), getContext().getDispatcher()).to(getSelf()); } /** @@ -736,9 +744,11 @@ public void onMutation(final Command command, final E event, final WithDittoH } @Override - public void onStagedMutation(final Command command, final CompletionStage event, + public void onStagedMutation(final Command command, + final CompletionStage event, final CompletionStage response, - final boolean becomeCreated, final boolean becomeDeleted) { + final boolean becomeCreated, + final boolean becomeDeleted) { final ActorRef sender = getSender(); persistAndApplyEventAsync(event, (persistedEvent, resultingEntity) -> { @@ -751,6 +761,16 @@ public void onStagedMutation(final Command command, final CompletionStage if (becomeCreated) { becomeCreatedHandler(); } + }, throwable -> { + final DittoRuntimeException dittoRuntimeException = + DittoRuntimeException.asDittoRuntimeException(throwable, t -> + DittoInternalErrorException.newBuilder() + .cause(t) + .dittoHeaders(command.getDittoHeaders()) + .build()); + if (shouldSendResponse(command.getDittoHeaders())) { + notifySender(sender, dittoRuntimeException); + } }); } @@ -766,7 +786,13 @@ public void onQuery(final Command command, final WithDittoHeaders response) { public void onStagedQuery(final Command command, final CompletionStage response) { if (command.getDittoHeaders().isResponseRequired()) { final ActorRef sender = getSender(); - response.thenAccept(r -> notifySender(sender, r)); + response.whenComplete((r, throwable) -> { + if (throwable instanceof DittoRuntimeException dittoRuntimeException) { + notifySender(sender, dittoRuntimeException); + } else { + notifySender(sender, r); + } + }); } } @@ -883,7 +909,13 @@ private void notifySender(final WithDittoHeaders message) { } private void notifySender(final ActorRef sender, final CompletionStage message) { - message.thenAccept(msg -> notifySender(sender, msg)); + message.whenComplete((msg, throwable) -> { + if (throwable instanceof DittoRuntimeException dittoRuntimeException) { + notifySender(sender, dittoRuntimeException); + } else { + notifySender(sender, msg); + } + }); } private void takeSnapshotByInterval(final Control takeSnapshot) { @@ -1097,19 +1129,23 @@ public void onQuery(final Command command, final WithDittoHeaders response) { @Override public void onStagedQuery(final Command command, final CompletionStage response) { if (command.getDittoHeaders().isResponseRequired()) { - response.thenAccept(r -> { - final WithDittoHeaders theResponseToSend; - if (response instanceof DittoHeadersSettable dittoHeadersSettable) { - final DittoHeaders queryCommandHeaders = r.getDittoHeaders(); - final DittoHeaders adjustedHeaders = queryCommandHeaders.toBuilder() - .putHeader(DittoHeaderDefinition.HISTORICAL_HEADERS.getKey(), - historicalDittoHeaders.toJson().toString()) - .build(); - theResponseToSend = dittoHeadersSettable.setDittoHeaders(adjustedHeaders); + response.whenComplete((r, throwable) -> { + if (throwable instanceof DittoRuntimeException dittoRuntimeException) { + notifySender(sender, dittoRuntimeException); } else { - theResponseToSend = r; + final WithDittoHeaders theResponseToSend; + if (response instanceof DittoHeadersSettable dittoHeadersSettable) { + final DittoHeaders queryCommandHeaders = r.getDittoHeaders(); + final DittoHeaders adjustedHeaders = queryCommandHeaders.toBuilder() + .putHeader(DittoHeaderDefinition.HISTORICAL_HEADERS.getKey(), + historicalDittoHeaders.toJson().toString()) + .build(); + theResponseToSend = dittoHeadersSettable.setDittoHeaders(adjustedHeaders); + } else { + theResponseToSend = r; + } + notifySender(sender, theResponseToSend); } - notifySender(sender, theResponseToSend); }); } } 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 ccb90e1d8c..1776501503 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 @@ -128,7 +128,7 @@ public abstract class AbstractPersistenceSupervisor>> get(final PolicyId key return delegate.get(key); } + @Override + public CompletableFuture>> get(final PolicyId key, + final Function>> errorHandler) { + return delegate.get(key, errorHandler); + } + @Override public CompletableFuture>> getIfPresent(final PolicyId key) { return delegate.getIfPresent(key); diff --git a/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java b/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java index 3622c9b517..dffd048f47 100755 --- a/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java +++ b/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java @@ -19,6 +19,12 @@ 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.persistence.RecoveryCompleted; +import org.eclipse.ditto.base.model.exceptions.DittoInternalErrorException; +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.headers.WithDittoHeaders; @@ -47,11 +53,6 @@ import org.eclipse.ditto.policies.service.persistence.actors.strategies.commands.PolicyCommandStrategies; import org.eclipse.ditto.policies.service.persistence.actors.strategies.events.PolicyEventStrategies; -import org.apache.pekko.actor.ActorRef; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.actor.Props; -import org.apache.pekko.persistence.RecoveryCompleted; - /** * PersistentActor which "knows" the state of a single {@link Policy}. */ @@ -279,6 +280,16 @@ public void onStagedMutation(final Command command, final CompletionStage { + final DittoRuntimeException dittoRuntimeException = + DittoRuntimeException.asDittoRuntimeException(throwable, t -> + DittoInternalErrorException.newBuilder() + .cause(t) + .dittoHeaders(command.getDittoHeaders()) + .build()); + if (shouldSendResponse(command.getDittoHeaders())) { + notifySender(sender, dittoRuntimeException); + } }); } diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingSupervisorActor.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingSupervisorActor.java index 6cd73ae28a..290e244ffe 100755 --- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingSupervisorActor.java +++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingSupervisorActor.java @@ -24,6 +24,19 @@ import javax.annotation.Nullable; +import org.apache.pekko.actor.ActorKilledException; +import org.apache.pekko.actor.ActorRef; +import org.apache.pekko.actor.ActorSelection; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.actor.Props; +import org.apache.pekko.japi.pf.FI; +import org.apache.pekko.japi.pf.ReceiveBuilder; +import org.apache.pekko.pattern.AskTimeoutException; +import org.apache.pekko.pattern.Patterns; +import org.apache.pekko.stream.Materializer; +import org.apache.pekko.stream.javadsl.Keep; +import org.apache.pekko.stream.javadsl.Sink; +import org.apache.pekko.stream.javadsl.Source; import org.eclipse.ditto.base.model.acks.DittoAcknowledgementLabel; import org.eclipse.ditto.base.model.auth.AuthorizationContext; import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; @@ -67,19 +80,6 @@ import org.eclipse.ditto.things.service.enforcement.ThingPolicyCreated; import org.eclipse.ditto.thingsearch.api.ThingsSearchConstants; -import org.apache.pekko.actor.ActorKilledException; -import org.apache.pekko.actor.ActorRef; -import org.apache.pekko.actor.ActorSelection; -import org.apache.pekko.actor.ActorSystem; -import org.apache.pekko.actor.Props; -import org.apache.pekko.japi.pf.FI; -import org.apache.pekko.japi.pf.ReceiveBuilder; -import org.apache.pekko.pattern.AskTimeoutException; -import org.apache.pekko.stream.Materializer; -import org.apache.pekko.stream.javadsl.Keep; -import org.apache.pekko.stream.javadsl.Sink; -import org.apache.pekko.stream.javadsl.Source; - /** * Supervisor for {@link ThingPersistenceActor} which means it will create, start and watch it as child actor. *

@@ -244,7 +244,10 @@ public static Props props(final ActorRef pubSubMediator, @Override protected CompletionStage askEnforcerChild(final Signal signal) { - if (signal instanceof ThingCommandResponse thingCommandResponse && + if (signal instanceof CreateThing createThing && createThing.getThing().getDefinition().isPresent()) { + // for thing creations containing a "definition", retrieving WoT model from URL is involved, give more time: + return Patterns.ask(enforcerChild, signal, localAskTimeout.multipliedBy(3)); + } else if (signal instanceof ThingCommandResponse thingCommandResponse && CommandResponse.isLiveCommandResponse(thingCommandResponse)) { return signal.getDittoHeaders().getCorrelationId() @@ -337,10 +340,11 @@ protected CompletionStage modifyTargetActorCommandResponse(final Signal< @Override protected CompletableFuture handleTargetActorAndEnforcerException(final Signal signal, final Throwable throwable) { if (RollbackCreatedPolicy.shouldRollbackBasedOnException(signal, throwable)) { + final Throwable cause = throwable.getCause(); log.withCorrelationId(signal) - .info("Target actor exception received: <{}>. " + + .info("Target actor exception received: <{}>, cause: <{}>. " + "Sending RollbackCreatedPolicy msg to self, potentially rolling back a created policy.", - throwable.getClass().getSimpleName()); + throwable.getClass().getSimpleName(), cause != null ? cause.getClass().getSimpleName() : "-"); final CompletableFuture responseFuture = new CompletableFuture<>(); getSelf().tell(RollbackCreatedPolicy.of(signal, throwable, responseFuture), getSelf()); return responseFuture; diff --git a/things/service/src/main/resources/things.conf b/things/service/src/main/resources/things.conf index ebd4048d3f..ef970e2704 100755 --- a/things/service/src/main/resources/things.conf +++ b/things/service/src/main/resources/things.conf @@ -167,6 +167,30 @@ ditto { expire-after-access = ${?THINGS_WOT_THING_MODEL_CACHE_EXPIRE_AFTER_ACCESS} } + tm-based-creation { + thing { + skeleton-creation-enabled = true + skeleton-creation-enabled = ${?THINGS_WOT_TM_BASED_CREATION_THING_SKELETON_CREATION_ENABLED} + + generate-defaults-for-optional-properties = false + generate-defaults-for-optional-properties = ${?THINGS_WOT_TM_BASED_CREATION_THING_GENERATE_DEFAULTS_FOR_OPTIONAL_PROPERTIES} + + throw-exception-on-wot-errors = true + throw-exception-on-wot-errors = ${?THINGS_WOT_TM_BASED_CREATION_THING_THROW_EXCEPTION_ON_WOT_ERRORS} + } + + feature { + skeleton-creation-enabled = true + skeleton-creation-enabled = ${?THINGS_WOT_TM_BASED_CREATION_FEATURE_SKELETON_CREATION_ENABLED} + + generate-defaults-for-optional-properties = false + generate-defaults-for-optional-properties = ${?THINGS_WOT_TM_BASED_CREATION_FEATURE_GENERATE_DEFAULTS_FOR_OPTIONAL_PROPERTIES} + + throw-exception-on-wot-errors = true + throw-exception-on-wot-errors = ${?THINGS_WOT_TM_BASED_CREATION_FEATURE_THROW_EXCEPTION_ON_WOT_ERRORS} + } + } + to-thing-description { base-prefix = "http://localhost:8080" base-prefix = ${?THINGS_WOT_TO_THING_DESCRIPTION_BASE_PREFIX} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java index a19e1c5c11..1039a202ef 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmBasedCreationConfig.java @@ -29,15 +29,12 @@ final class DefaultTmBasedCreationConfig implements TmBasedCreationConfig { private static final String CONFIG_PATH = "tm-based-creation"; - private final boolean thingSkeletonCreationEnabled; - private final boolean featureSkeletonCreationEnabled; - + private final TmScopedCreationConfig thingCreationConfig; + private final TmScopedCreationConfig featureCreationConfig; private DefaultTmBasedCreationConfig(final ScopedConfig scopedConfig) { - thingSkeletonCreationEnabled = - scopedConfig.getBoolean(ConfigValue.THING_SKELETON_CREATION_ENABLED.getConfigPath()); - featureSkeletonCreationEnabled = - scopedConfig.getBoolean(ConfigValue.FEATURE_SKELETON_CREATION_ENABLED.getConfigPath()); + thingCreationConfig = DefaultTmScopedCreationConfig.of(scopedConfig, "thing"); + featureCreationConfig = DefaultTmScopedCreationConfig.of(scopedConfig, "feature"); } /** @@ -52,15 +49,14 @@ public static DefaultTmBasedCreationConfig of(final Config config) { ConfigValue.values())); } - @Override - public boolean isThingSkeletonCreationEnabled() { - return thingSkeletonCreationEnabled; + public TmScopedCreationConfig getThingCreationConfig() { + return thingCreationConfig; } @Override - public boolean isFeatureSkeletonCreationEnabled() { - return featureSkeletonCreationEnabled; + public TmScopedCreationConfig getFeatureCreationConfig() { + return featureCreationConfig; } @Override @@ -72,20 +68,20 @@ public boolean equals(final Object o) { return false; } final DefaultTmBasedCreationConfig that = (DefaultTmBasedCreationConfig) o; - return thingSkeletonCreationEnabled == that.thingSkeletonCreationEnabled && - featureSkeletonCreationEnabled == that.featureSkeletonCreationEnabled; + return Objects.equals(thingCreationConfig, that.thingCreationConfig) && + Objects.equals(featureCreationConfig, that.featureCreationConfig); } @Override public int hashCode() { - return Objects.hash(thingSkeletonCreationEnabled, featureSkeletonCreationEnabled); + return Objects.hash(thingCreationConfig, featureCreationConfig); } @Override public String toString() { return getClass().getSimpleName() + " [" + - "thingSkeletonCreationEnabled=" + thingSkeletonCreationEnabled + - ", featureSkeletonCreationEnabled=" + featureSkeletonCreationEnabled + + "thingCreationConfig=" + thingCreationConfig + + ", featureCreationConfig=" + featureCreationConfig + "]"; } } diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmScopedCreationConfig.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmScopedCreationConfig.java new file mode 100644 index 0000000000..f56139c3a0 --- /dev/null +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/DefaultTmScopedCreationConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.wot.integration.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 com.typesafe.config.Config; + +/** + * This class is the default implementation of the WoT (Web of Things) {@link TmScopedCreationConfig}. + */ +@Immutable +final class DefaultTmScopedCreationConfig implements TmScopedCreationConfig { + + private final boolean skeletonCreationEnabled; + private final boolean generateDefaultsForOptionalProperties; + private final boolean throwExceptionOnWotErrors; + + private DefaultTmScopedCreationConfig(final ScopedConfig scopedConfig) { + skeletonCreationEnabled = + scopedConfig.getBoolean(ConfigValue.SKELETON_CREATION_ENABLED.getConfigPath()); + generateDefaultsForOptionalProperties = + scopedConfig.getBoolean(ConfigValue.GENERATE_DEFAULTS_FOR_OPTIONAL_PROPERTIES.getConfigPath()); + throwExceptionOnWotErrors = + scopedConfig.getBoolean(ConfigValue.THROW_EXCEPTION_ON_WOT_ERRORS.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 {@code configPath}. + * @return the instance. + * @throws org.eclipse.ditto.internal.utils.config.DittoConfigError if {@code config} is invalid. + */ + public static DefaultTmScopedCreationConfig of(final Config config, final String configPath) { + return new DefaultTmScopedCreationConfig(ConfigWithFallback.newInstance(config, configPath, + ConfigValue.values())); + } + + @Override + public boolean isSkeletonCreationEnabled() { + return skeletonCreationEnabled; + } + + @Override + public boolean shouldGenerateDefaultsForOptionalProperties() { + return generateDefaultsForOptionalProperties; + } + + @Override + public boolean shouldThrowExceptionOnWotErrors() { + return throwExceptionOnWotErrors; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DefaultTmScopedCreationConfig that = (DefaultTmScopedCreationConfig) o; + return skeletonCreationEnabled == that.skeletonCreationEnabled && + generateDefaultsForOptionalProperties == that.generateDefaultsForOptionalProperties && + throwExceptionOnWotErrors == that.throwExceptionOnWotErrors; + } + + @Override + public int hashCode() { + return Objects.hash(skeletonCreationEnabled, generateDefaultsForOptionalProperties, throwExceptionOnWotErrors); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "skeletonCreationEnabled=" + skeletonCreationEnabled + + "generateDefaultsForOptionalProperties=" + generateDefaultsForOptionalProperties + + "throwExceptionOnWotErrors=" + throwExceptionOnWotErrors + + "]"; + } +} diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java index 62a27e64a0..564bb98c78 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmBasedCreationConfig.java @@ -26,22 +26,14 @@ public interface TmBasedCreationConfig { /** - * Returns whether the creation of a Thing skeleton based on an on creation contained WoT ThingModel in the - * Thing's {@code ThingDefinition} for {@code CreateThing} commands should be enabled or not. - * - * @return whether the TM based Thing skeleton creation should be enabled or not. + * @return the TM based creation configuration for creating things. */ - boolean isThingSkeletonCreationEnabled(); + TmScopedCreationConfig getThingCreationConfig(); /** - * Returns whether the creation of a Feature skeleton based on an on creation contained WoT ThingModel in the - * Features' {@code FeatureDefinition} for {@code ModifyFeature} commands (which create a new feature) - * should be enabled or not. - * - * @return whether the TM based Feature skeleton creation should be enabled or not. + * @return the TM based creation configuration for creating features. */ - boolean isFeatureSkeletonCreationEnabled(); - + TmScopedCreationConfig getFeatureCreationConfig(); /** * An enumeration of the known config path expressions and their associated default values for diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmScopedCreationConfig.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmScopedCreationConfig.java new file mode 100644 index 0000000000..9735b76808 --- /dev/null +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/config/TmScopedCreationConfig.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.wot.integration.config; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.internal.utils.config.KnownConfigValue; + +/** + * Provides configuration settings for WoT (Web of Things) integration regarding the creation of Thing and Feature + * skeletons based on WoT ThingModels. + * + * @since 3.5.0 + */ +@Immutable +public interface TmScopedCreationConfig { + + /** + * Returns whether the creation of a Thing/Feature skeleton based on an on creation contained WoT ThingModel in the + * Thing's {@code ThingDefinition} for {@code CreateThing} commands should be enabled or not. + * + * @return whether the TM based Thing skeleton creation should be enabled or not. + */ + boolean isSkeletonCreationEnabled(); + + /** + * Returns whether for optional marked properties in the WoT ThingModel properties should be generated based on their + * defaults. + * + * @return whether for optional marked properties in the WoT ThingModel properties should be generated. + */ + boolean shouldGenerateDefaultsForOptionalProperties(); + + /** + * Returns whether for WoT related errors (e.g. not downloadable WoT ThingModel or not parsable ThingModel) exceptions + * should be thrown, or they should be swallowed silently and as result no skeleton should be created. + * + * @return whether for WoT related errors exceptions should be thrown + */ + boolean shouldThrowExceptionOnWotErrors(); + + /** + * An enumeration of the known config path expressions and their associated default values for + * {@code TmScopedCreationConfig}. + */ + enum ConfigValue implements KnownConfigValue { + + /** + * Whether the TM based Thing skeleton creation should be enabled or not. + */ + SKELETON_CREATION_ENABLED("skeleton-creation-enabled", true), + + /** + * Whether for the Thing skeleton creation, defaults for optional properties should be generated. + */ + GENERATE_DEFAULTS_FOR_OPTIONAL_PROPERTIES("generate-defaults-for-optional-properties", false), + + /** + * Whether during Thing skeleton creation, exceptions should be thrown if e.g. a WoT model could not be resolved + * or was invalid. + */ + THROW_EXCEPTION_ON_WOT_ERRORS("throw-exception-on-wot-errors", 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/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java index c8a50e5879..32534431ff 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/DefaultWotThingSkeletonGenerator.java @@ -101,6 +101,7 @@ final class DefaultWotThingSkeletonGenerator implements WotThingSkeletonGenerato public CompletionStage> generateThingSkeleton(final ThingId thingId, final ThingModel thingModel, final URL thingModelUrl, + final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { return thingModelExtensionResolver @@ -124,6 +125,7 @@ public CompletionStage> generateThingSkeleton(final ThingId thin fillPropertiesInOptionalCategories( properties, + generateDefaultsForOptionalProperties, thingModelWithExtensionsAndImports.getTmOptional().orElse(null), jsonObjectBuilder, attributesCategories, @@ -137,7 +139,7 @@ public CompletionStage> generateThingSkeleton(final ThingId thin ); final AttributesBuilder attributesBuilder = Attributes.newBuilder(); - if (attributesCategories.size() > 0) { + if (!attributesCategories.isEmpty()) { attributesCategories.forEach((attributeCategory, categoryObjBuilder) -> attributesBuilder.set(attributeCategory, categoryObjBuilder.build()) ); @@ -149,26 +151,27 @@ public CompletionStage> generateThingSkeleton(final ThingId thin return Pair.apply(thingModelWithExtensionsAndImports, builder); }) .thenCompose(pair -> - createFeaturesFromSubmodels(pair.first(), dittoHeaders) - .thenApply(features -> - features.map(f -> pair.second().setFeatures(f)).orElse(pair.second()) - ) + createFeaturesFromSubmodels(pair.first(), generateDefaultsForOptionalProperties, dittoHeaders) + .thenApply(features -> + features.map(f -> pair.second().setFeatures(f)).orElse(pair.second()) + ) ) .thenApply(builder -> Optional.of(builder.build())); } private static void fillPropertiesInOptionalCategories(final Properties properties, + final boolean generateDefaultsForOptionalProperties, @Nullable final TmOptional tmOptionalElements, final JsonObjectBuilder jsonObjectBuilder, final Map propertiesCategories, final Function> propertyCategoryExtractor) { properties.values().stream() - // filter out optional elements - don't create skeleton values for those: - .filter(property -> Optional.ofNullable(tmOptionalElements) + .filter(property -> generateDefaultsForOptionalProperties || Optional.ofNullable(tmOptionalElements) .stream() .noneMatch(optionals -> optionals.stream() .anyMatch(optionalEl -> + // filter out optional elements - don't create skeleton values for those: optionalEl.toString().equals("/properties/" + property.getPropertyName()) ) ) @@ -190,7 +193,7 @@ private static void fillPropertiesInOptionalCategories(final Properties properti } private CompletionStage> createFeaturesFromSubmodels(final ThingModel thingModel, - final DittoHeaders dittoHeaders) { + final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { final FeaturesBuilder featuresBuilder = Features.newBuilder(); final List>> futureList = thingModel.getLinks() @@ -219,6 +222,7 @@ private CompletionStage> createFeaturesFromSubmodels(final Th generateFeatureSkeleton(submodel.instanceName, subThingModel, submodel.href, + generateDefaultsForOptionalProperties, dittoHeaders ), executor) .toCompletableFuture() @@ -245,9 +249,11 @@ private CompletionStage> createFeaturesFromSubmodels(final Th private CompletionStage> generateFeatureSkeleton(final String featureId, final ThingModel thingModel, final IRI thingModelIri, + final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { try { - return generateFeatureSkeleton(featureId, thingModel, new URL(thingModelIri.toString()), dittoHeaders); + return generateFeatureSkeleton(featureId, thingModel, new URL(thingModelIri.toString()), + generateDefaultsForOptionalProperties, dittoHeaders); } catch (final MalformedURLException e) { throw ThingDefinitionInvalidException.newBuilder(thingModelIri) .dittoHeaders(dittoHeaders) @@ -259,6 +265,7 @@ private CompletionStage> generateFeatureSkeleton(final String public CompletionStage> generateFeatureSkeleton(final String featureId, final ThingModel thingModel, final URL thingModelUrl, + final boolean generateDefaultsForOptionalProperties, final DittoHeaders dittoHeaders) { return thingModelExtensionResolver @@ -284,6 +291,7 @@ public CompletionStage> generateFeatureSkeleton(final String f fillPropertiesInOptionalCategories( properties, + generateDefaultsForOptionalProperties, thingModelWithExtensionsAndImports.getTmOptional().orElse(null), jsonObjectBuilder, propertiesCategories, @@ -298,7 +306,7 @@ public CompletionStage> generateFeatureSkeleton(final String f final FeaturePropertiesBuilder propertiesBuilder = FeatureProperties.newBuilder(); - if (propertiesCategories.size() > 0) { + if (!propertiesCategories.isEmpty()) { propertiesCategories.forEach((propertyCategory, categoryObjBuilder) -> propertiesBuilder.set(propertyCategory, categoryObjBuilder.build()) ); @@ -465,7 +473,8 @@ private static String provideNeutralStringElement(@Nullable final Integer minLen return ""; } - private CompletionStage resolveFeatureDefinition(final ThingModel thingModel, final URL thingModelUrl, + private CompletionStage resolveFeatureDefinition(final ThingModel thingModel, + final URL thingModelUrl, final DittoHeaders dittoHeaders) { return determineFurtherFeatureDefinitionIdentifiers(thingModel, dittoHeaders) .thenApply(definitionIdentifiers -> FeatureDefinition.fromIdentifier( diff --git a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java index f2247ebe90..c9c724d508 100644 --- a/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java +++ b/wot/integration/src/main/java/org/eclipse/ditto/wot/integration/generator/WotThingSkeletonGenerator.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.concurrent.CompletionStage; +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.Thing; @@ -23,8 +24,6 @@ import org.eclipse.ditto.wot.integration.provider.WotThingModelFetcher; import org.eclipse.ditto.wot.model.ThingModel; -import org.apache.pekko.actor.ActorSystem; - /** * Generator for WoT (Web of Things) based Ditto {@link Thing}s and {@link Feature} skeletons based on * a given WoT {@link org.eclipse.ditto.wot.model.ThingModel} for creation of Ditto entities (Things/Features). @@ -51,9 +50,38 @@ public interface WotThingSkeletonGenerator { * @throws org.eclipse.ditto.wot.model.WotThingModelInvalidException if the WoT ThingModel did not contain the * mandatory {@code "@type"} being {@code "tm:ThingModel"} */ + default CompletionStage> generateThingSkeleton(ThingId thingId, + ThingModel thingModel, + URL thingModelUrl, + DittoHeaders dittoHeaders) { + return generateThingSkeleton(thingId, thingModel, thingModelUrl, false, dittoHeaders); + } + + /** + * Generates a skeleton {@link Thing} for the given {@code thingId}. + * Uses the passed in {@code thingModel} and generates + *
    + *
  • {@code attributes} on Thing level for all required properties in that TM
  • + *
  • Features for all included TM submodels (links with {@code tm:submodel} type)
  • + *
  • Feature properties on Feature levels for all required properties in linked submodel TMs
  • + *
+ * + * @param thingId the ThingId to generate the Thing skeleton for. + * @param thingModel the ThingModel to use as template for generating the Thing skeleton. + * @param thingModelUrl the URL from which the ThingModel was fetched. + * @param generateDefaultsForOptionalProperties whether for optional marked properties in the WoT ThingModel + * properties should be generated based on their defaults. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeException which might occur during the + * generation. + * @return the generated Thing skeleton 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"} + * @since 3.5.0 + */ CompletionStage> generateThingSkeleton(ThingId thingId, ThingModel thingModel, URL thingModelUrl, + boolean generateDefaultsForOptionalProperties, DittoHeaders dittoHeaders); /** @@ -72,9 +100,36 @@ CompletionStage> generateThingSkeleton(ThingId thingId, * @throws org.eclipse.ditto.wot.model.WotThingModelInvalidException if the WoT ThingModel did not contain the * mandatory {@code "@type"} being {@code "tm:ThingModel"} */ + default CompletionStage> generateFeatureSkeleton(String featureId, + ThingModel thingModel, + URL thingModelUrl, + DittoHeaders dittoHeaders) { + return generateFeatureSkeleton(featureId, thingModel, thingModelUrl, false, dittoHeaders); + } + + /** + * Generates a skeleton {@link Feature} for the given {@code featureId}. + * Uses the passed in {@code thingModel} and generates + *
    + *
  • {@code properties} for all required properties in that TM
  • + *
+ * + * @param featureId the FeatureId to generate + * @param thingModel the ThingModel to use as template for generating the Feature skeleton. + * @param thingModelUrl the URL from which the ThingModel was fetched. + * @param generateDefaultsForOptionalProperties whether for optional marked properties in the WoT ThingModel + * properties should be generated based on their defaults. + * @param dittoHeaders the DittoHeaders for possibly thrown DittoRuntimeException which might occur during the + * generation. + * @return the generated Feature skeleton for the given {@code featureId} 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"} + * @since 3.5.0 + */ CompletionStage> generateFeatureSkeleton(String featureId, ThingModel thingModel, URL thingModelUrl, + boolean generateDefaultsForOptionalProperties, DittoHeaders dittoHeaders); /** 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 index e20d8578f2..4e51f11e45 100644 --- 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 @@ -121,7 +121,7 @@ public CompletionStage> provideThingSkeletonForCreation(final Th final ThreadSafeDittoLogger logger = LOGGER.withCorrelationId(dittoHeaders); if (FeatureToggle.isWotIntegrationFeatureEnabled() && - wotConfig.getCreationConfig().isThingSkeletonCreationEnabled() && + wotConfig.getCreationConfig().getThingCreationConfig().isSkeletonCreationEnabled() && null != thingDefinition) { final Optional urlOpt = thingDefinition.getUrl(); if (urlOpt.isPresent()) { @@ -129,22 +129,40 @@ public CompletionStage> provideThingSkeletonForCreation(final Th 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, dittoHeaders), + .thenComposeAsync(thingModel -> thingSkeletonGenerator.generateThingSkeleton( + thingId, + thingModel, + url, + wotConfig.getCreationConfig() + .getThingCreationConfig() + .shouldGenerateDefaultsForOptionalProperties(), + dittoHeaders + ), executor ) - .thenApply(thingSkeleton -> { - logger.debug("Created Thing skeleton for new Thing with id <{}>: <{}>", thingId, - thingSkeleton); - return thingSkeleton; - }) - .exceptionally(throwable -> { - logger.info("Could not fetch ThingModel or generate Thing skeleton based on it due " + - "to: <{}: {}>", - throwable.getClass().getSimpleName(),throwable.getMessage(), throwable); - return Optional.empty(); + .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()); } @@ -159,7 +177,7 @@ public CompletionStage> provideFeatureSkeletonForCreation(fina final ThreadSafeDittoLogger logger = LOGGER.withCorrelationId(dittoHeaders); if (FeatureToggle.isWotIntegrationFeatureEnabled() && - wotConfig.getCreationConfig().isFeatureSkeletonCreationEnabled() && + wotConfig.getCreationConfig().getFeatureCreationConfig().isSkeletonCreationEnabled() && null != featureDefinition) { final Optional urlOpt = featureDefinition.getFirstIdentifier().getUrl(); if (urlOpt.isPresent()) { @@ -167,20 +185,39 @@ public CompletionStage> provideFeatureSkeletonForCreation(fina 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, dittoHeaders), + .thenComposeAsync(thingModel -> thingSkeletonGenerator.generateFeatureSkeleton( + featureId, + thingModel, + url, + wotConfig.getCreationConfig() + .getFeatureCreationConfig() + .shouldGenerateDefaultsForOptionalProperties(), + dittoHeaders + ), executor ) - .thenApply(featureSkeleton -> { - logger.debug("Created Feature skeleton for new Feature with id <{}>: <{}>", featureId, - featureSkeleton); - return featureSkeleton; - }) - .exceptionally(throwable -> { - logger.info("Could not fetch ThingModel or generate Feature skeleton based on it " + - "due to: <{}: {}>", - throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); - return Optional.empty(); + .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()); @@ -203,19 +240,19 @@ private CompletionStage getWotThingDescriptionForThing(final T 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 - ), + .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 -> { @@ -247,18 +284,18 @@ private CompletionStage getWotThingDescriptionForFeature(final 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 - ), + .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 -> {