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..2da20705a4 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) { 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 a9ad37378f..324812fe80 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 @@ -125,12 +125,18 @@ protected Result> doApply(final Context context, .build() ); + // validate based on potentially referenced Thing WoT TM/TD + final CompletionStage validatedStage = thingStage.thenCompose(createdThing -> wotThingModelValidator + .validateThing(createdThing, command.getResourcePath(), command.getDittoHeaders()) + .thenApply(aVoid -> createdThing) + ); + final CompletionStage> eventStage = - thingStage.thenApply(newThingWithImplicits -> + validatedStage.thenApply(newThingWithImplicits -> ThingCreated.of(newThingWithImplicits, nextRevision, now, commandHeaders, metadata) ); - final CompletionStage responseStage = thingStage.thenApply(newThingWithImplicits -> + final CompletionStage responseStage = validatedStage.thenApply(newThingWithImplicits -> appendETagHeaderIfProvided(command, CreateThingResponse.of(newThingWithImplicits, commandHeaders), newThingWithImplicits) ); 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 dc38fba537..4117e8428d 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,6 +13,7 @@ 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; @@ -91,13 +92,27 @@ private Result> getModifyResult(final Context context, fi final JsonPointer attributePointer = command.getAttributePointer(); final DittoHeaders dittoHeaders = command.getDittoHeaders(); - final ThingEvent event = + final CompletionStage validatedStage = getValidatedStage(command, thing); + final CompletionStage> eventStage = validatedStage.thenApply(aVoid -> AttributeModified.of(thingId, attributePointer, command.getAttributeValue(), nextRevision, - getEventTimestamp(), dittoHeaders, metadata); - final WithDittoHeaders response = appendETagHeaderIfProvided(command, - ModifyAttributeResponse.modified(thingId, attributePointer, dittoHeaders), thing); + getEventTimestamp(), dittoHeaders, metadata) + ); + final CompletionStage responseStage = validatedStage.thenApply(aVoid -> + appendETagHeaderIfProvided(command, + ModifyAttributeResponse.modified(thingId, attributePointer, dittoHeaders), thing) + ); + + return ResultFactory.newMutationResult(command, eventStage, responseStage); + } - return ResultFactory.newMutationResult(command, event, response); + private CompletionStage getValidatedStage(final ModifyAttribute command, @Nullable final Thing thing) { + return wotThingModelValidator + .validateThingAttribute(Optional.ofNullable(thing).flatMap(Thing::getDefinition).orElse(null), + command.getAttributePointer(), + command.getAttributeValue(), + command.getResourcePath(), + command.getDittoHeaders() + ); } private Result> getCreateResult(final Context context, final long nextRevision, @@ -108,13 +123,17 @@ private Result> getCreateResult(final Context context, fi final JsonValue attributeValue = command.getAttributeValue(); final DittoHeaders dittoHeaders = command.getDittoHeaders(); - final ThingEvent event = + final CompletionStage validatedStage = getValidatedStage(command, thing); + final CompletionStage> eventStage = validatedStage.thenApply(aVoid -> 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(aVoid -> + appendETagHeaderIfProvided(command, + 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 5f3f1fa6e1..5d54cb03a1 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,6 +13,7 @@ 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; @@ -87,27 +88,42 @@ private Result> getModifyResult(final Context context, fi 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); + final CompletionStage validatedStage = getValidatedStage(command, thing); + final CompletionStage> eventStage = validatedStage.thenApply(attributes -> + AttributesModified.of(thingId, attributes, nextRevision, getEventTimestamp(), dittoHeaders, metadata) + ); + final CompletionStage responseStage = validatedStage.thenApply(attributes -> + appendETagHeaderIfProvided(command, ModifyAttributesResponse.modified(thingId, dittoHeaders), thing) + ); - return ResultFactory.newMutationResult(command, event, response); + return ResultFactory.newMutationResult(command, eventStage, responseStage); + } + + private CompletionStage getValidatedStage(final ModifyAttributes command, @Nullable final Thing thing) { + return wotThingModelValidator + .validateThingAttributes(Optional.ofNullable(thing).flatMap(Thing::getDefinition).orElse(null), + command.getAttributes(), + command.getResourcePath(), + command.getDittoHeaders() + ) + .thenApply(aVoid -> command.getAttributes()); } 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); + final CompletionStage validatedStage = getValidatedStage(command, thing); + final CompletionStage> eventStage = validatedStage.thenApply(attributes -> + AttributesCreated.of(thingId, attributes, nextRevision, getEventTimestamp(), dittoHeaders, metadata) + ); + final CompletionStage responseStage = validatedStage.thenApply(attributes -> + appendETagHeaderIfProvided(command, ModifyAttributesResponse.created(thingId, attributes, dittoHeaders), + thing) + ); - return ResultFactory.newMutationResult(command, event, 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/ModifyFeatureDefinitionStrategy.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ModifyFeatureDefinitionStrategy.java index 9549963b50..eee2948e57 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 @@ -55,6 +55,8 @@ protected Result> doApply(final Context context, final ModifyFeatureDefinition command, @Nullable final Metadata metadata) { + // TODO TJ we probably must also validate the current feature properties based on the new feature definition + final String featureId = command.getFeatureId(); return extractFeature(command, thing) 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 17cda488ff..73601a50e7 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 @@ -95,7 +95,12 @@ protected Result> doApply(final Context context, // validate based on potentially referenced Feature WoT TM final CompletionStage validatedStage; if (featureDefinition.isPresent()) { - validatedStage = wotThingModelValidator.validateFeature(feature, command.getDittoHeaders()); + validatedStage = wotThingModelValidator.validateFeature( + nonNullThing.getDefinition().orElse(null), + feature, + command.getResourcePath(), + command.getDittoHeaders() + ); } else { validatedStage = CompletableFuture.completedStage(null); } @@ -169,7 +174,12 @@ private Result> getCreateResult(final Context context, fi ); final Function> validationFunction = feature -> - wotThingModelValidator.validateFeature(feature, command.getDittoHeaders()) + wotThingModelValidator.validateFeature( + Optional.ofNullable(thing).flatMap(Thing::getDefinition).orElse(null), + feature, + command.getResourcePath(), + command.getDittoHeaders() + ) .thenApply(aVoid -> feature); final CompletionStage> eventStage = featureStage.thenCompose(validationFunction).thenApply(feature -> 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 d08ac878b5..f4752dc60b 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,6 +19,7 @@ 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; @@ -79,7 +80,8 @@ protected Result> 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,13 +101,30 @@ private Result> getModifyResult(final Context context, fi 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 = getValidationStage(command, command.getFeatures(), thing); + final CompletionStage> eventStage = validationStage.thenApply(features -> + FeaturesModified.of(command.getEntityId(), features, nextRevision, + getEventTimestamp(), dittoHeaders, metadata) + ); + final CompletionStage responseStage = validationStage.thenApply(features -> + appendETagHeaderIfProvided(command, + ModifyFeaturesResponse.modified(context.getState(), dittoHeaders), thing) + ); - return ResultFactory.newMutationResult(command, event, response); + return ResultFactory.newMutationResult(command, eventStage, responseStage); + } + + private CompletionStage getValidationStage(final ModifyFeatures command, + final Features features, + @Nullable final Thing thing + ) { + return wotThingModelValidator.validateFeatures( + Optional.ofNullable(thing).flatMap(Thing::getDefinition).orElse(null), + features, + command.getResourcePath(), + command.getDittoHeaders() + ) + .thenApply(aVoid -> command.getFeatures()); } private Result> getCreateResult(final Context context, final long nextRevision, @@ -159,14 +178,18 @@ 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 -> + getValidationStage(command, features, thing) + .thenApply(aVoid -> features); + final CompletableFuture> eventStage = + featuresStage.thenCompose(validationFunction).thenApply(features -> + FeaturesCreated.of(command.getEntityId(), features, nextRevision, getEventTimestamp(), + dittoHeaders, metadata + ) + ); final CompletableFuture responseStage = - featuresStage.thenApply(features -> appendETagHeaderIfProvided(command, + featuresStage.thenCompose(validationFunction).thenApply(features -> appendETagHeaderIfProvided(command, ModifyFeaturesResponse.created(context.getState(), features, dittoHeaders), thing) ); 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 aeba8b012c..5e3050ac01 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 @@ -55,6 +55,10 @@ protected Result> doApply(final Context context, final ModifyThingDefinition command, @Nullable final Metadata metadata) { + // TODO TJ when changing the thing "definition", we must also validate the complete thing against the new definition + // and fail the request if it does not match + // only that way, we can support upgrading a WoT model version + return extractDefinition(thing) .map(definition -> getModifyResult(context, nextRevision, command, thing, metadata)) .orElseGet(() -> getCreateResult(context, nextRevision, command, thing, metadata)); 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 c5ddb0f9ae..8d24bc6841 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 @@ -99,16 +99,17 @@ private Result> applyModifyCommand(final Context context, final Thing modifiedThing = applyThingModifications(command.getThing(), thing, eventTs, nextRevision); // validate based on potentially referenced Thing WoT TM/TD - final CompletionStage validatedStage = wotThingModelValidator - .validateThing(modifiedThing, command.getDittoHeaders()); + final CompletionStage validatedStage = wotThingModelValidator + .validateThing(modifiedThing, command.getResourcePath(), command.getDittoHeaders()) + .thenApply(aVoid -> modifiedThing); final CompletionStage> eventStage = - validatedStage.thenApply(aVoid -> - ThingModified.of(modifiedThing, nextRevision, eventTs, dittoHeaders, metadata)); + validatedStage.thenApply(theThing -> + ThingModified.of(theThing, nextRevision, eventTs, dittoHeaders, metadata)); final CompletionStage responseStage = - validatedStage.thenApply(aVoid -> + validatedStage.thenApply(theThing -> appendETagHeaderIfProvided(command, - ModifyThingResponse.modified(context.getState(), dittoHeaders), modifiedThing)); + ModifyThingResponse.modified(context.getState(), dittoHeaders), theThing)); return ResultFactory.newMutationResult(command, eventStage, responseStage); } diff --git a/things/service/src/main/resources/things.conf b/things/service/src/main/resources/things.conf index a838f70827..8bf4c2e670 100755 --- a/things/service/src/main/resources/things.conf +++ b/things/service/src/main/resources/things.conf @@ -246,7 +246,7 @@ ditto { } tm-model-validation { - enabled = true # TODO TJ default should be false + enabled = true # TODO TJ default should be false to "opt in" into the WoT validation explicitly enabled = ${?THINGS_WOT_TM_MODEL_VALIDATION_ENABLED} thing { @@ -261,6 +261,9 @@ ditto { } feature { + enforce-presence-of-modeled-features = true + allow-non-modeled-features = false + enforce-properties = true allow-non-modeled-properties = false 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 index 63f131bf19..d114b85b1b 100644 --- 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 @@ -30,6 +30,8 @@ final class DefaultFeatureValidationConfig implements FeatureValidationConfig { private static final String CONFIG_PATH = "feature"; + private final boolean enforcePresenceOfModeledFeatures; + private final boolean allowNonModeledFeatures; private final boolean enforceProperties; private final boolean allowNonModeledProperties; private final boolean enforceDesiredProperties; @@ -40,6 +42,10 @@ final class DefaultFeatureValidationConfig implements FeatureValidationConfig { private final boolean allowNonModeledOutboxMessages; private DefaultFeatureValidationConfig(final ScopedConfig scopedConfig) { + enforcePresenceOfModeledFeatures = + scopedConfig.getBoolean(ConfigValue.ENFORCE_PRESENCE_OF_MODELED_FEATURES.getConfigPath()); + allowNonModeledFeatures = + scopedConfig.getBoolean(ConfigValue.ALLOW_NON_MODELED_FEATURES.getConfigPath()); enforceProperties = scopedConfig.getBoolean(ConfigValue.ENFORCE_PROPERTIES.getConfigPath()); allowNonModeledProperties = @@ -70,6 +76,16 @@ public static DefaultFeatureValidationConfig of(final Config config) { ConfigValue.values())); } + @Override + public boolean isEnforcePresenceOfModeledFeatures() { + return enforcePresenceOfModeledFeatures; + } + + @Override + public boolean isAllowNonModeledFeatures() { + return allowNonModeledFeatures; + } + @Override public boolean isEnforceProperties() { return enforceProperties; @@ -115,7 +131,9 @@ 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 enforceProperties == that.enforceProperties && + return enforcePresenceOfModeledFeatures == that.enforcePresenceOfModeledFeatures && + allowNonModeledFeatures == that.allowNonModeledFeatures && + enforceProperties == that.enforceProperties && allowNonModeledProperties == that.allowNonModeledProperties && enforceDesiredProperties == that.enforceDesiredProperties && allowNonModeledDesiredProperties == that.allowNonModeledDesiredProperties && @@ -127,15 +145,18 @@ public boolean equals(final Object o) { @Override public int hashCode() { - return Objects.hash(enforceProperties, allowNonModeledProperties, enforceDesiredProperties, - allowNonModeledDesiredProperties, enforceInboxMessages, allowNonModeledInboxMessages, - enforceOutboxMessages, allowNonModeledOutboxMessages); + return Objects.hash(enforcePresenceOfModeledFeatures, allowNonModeledFeatures, enforceProperties, + allowNonModeledProperties, enforceDesiredProperties, allowNonModeledDesiredProperties, + enforceInboxMessages, allowNonModeledInboxMessages, enforceOutboxMessages, + allowNonModeledOutboxMessages); } @Override public String toString() { return getClass().getSimpleName() + " [" + - "enforceProperties=" + enforceProperties + + "enforcePresenceOfModeledFeatures=" + enforcePresenceOfModeledFeatures + + ", allowNonModeledFeatures=" + allowNonModeledFeatures + + ", enforceProperties=" + enforceProperties + ", allowNonModeledProperties=" + allowNonModeledProperties + ", enforceDesiredProperties=" + enforceDesiredProperties + ", allowNonModeledDesiredProperties=" + allowNonModeledDesiredProperties + diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java index 5a778fd0c6..732b02b229 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingDescriptionGenerator.java @@ -65,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; diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java index 9c76149d88..c853823f53 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DefaultWotThingSkeletonGenerator.java @@ -60,6 +60,7 @@ 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; diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java index c2c885c119..9c574a7446 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java @@ -22,15 +22,22 @@ 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.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.WotThingModelValidation; @@ -56,91 +63,268 @@ final class DefaultWotThingModelValidator implements WotThingModelValidator { } @Override - public CompletionStage validateThing(final Thing thing, final DittoHeaders dittoHeaders) { - + public CompletionStage validateThing(final Thing thing, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled()) { final Optional urlOpt = thing.getDefinition().flatMap(DefinitionIdentifier::getUrl); if (urlOpt.isPresent()) { final URL url = urlOpt.get(); final Function> validationFunction = thingModelWithExtensionsAndImports -> - validateThing(thingModelWithExtensionsAndImports, thing, dittoHeaders); + validateThing(thingModelWithExtensionsAndImports, thing, resourcePath, dittoHeaders); return fetchResolveAndValidateWith(url, dittoHeaders, validationFunction); } else { - return CompletableFuture.completedStage(null); + return success(); } } else { - return CompletableFuture.completedStage(null); + return success(); } } @Override public CompletionStage validateThing(final ThingModel thingModel, final Thing thing, - final DittoHeaders dittoHeaders) { - + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled()) { - return thingModelValidation.validateThingAttributes(thingModel, thing, dittoHeaders) - .thenCompose(aVoid -> - thing.getFeatures().map(features -> - thingModelResolver.resolveThingModelSubmodels(thingModel, dittoHeaders) - .thenComposeAsync(subModels -> - thingModelValidation.validateFeaturesProperties( - subModels.entrySet().stream().collect( - Collectors.toMap( - e -> e.getKey().instanceName(), - Map.Entry::getValue, - (a, b) -> a, - LinkedHashMap::new - ) - ), features, dittoHeaders - ), - executor - ) - ).orElse(CompletableFuture.completedStage(null)) - ); + return validateThingAttributes(thingModel, thing.getAttributes().orElse(null), resourcePath, + dittoHeaders + ).thenCompose(aVoid -> + thingModelResolver.resolveThingModelSubmodels(thingModel, dittoHeaders) + .thenCompose(subModels -> + doValidateFeatures(subModels, thing.getFeatures().orElse(null), resourcePath, + dittoHeaders) + ) + ); } else { - return CompletableFuture.completedStage(null); + return success(); } } @Override - public CompletionStage validateFeature(final Feature feature, final DittoHeaders dittoHeaders) { + public CompletionStage validateThingAttributes(@Nullable final ThingDefinition thingDefinition, + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled() && + thingDefinition != null && attributes != null) { + final Optional urlOpt = thingDefinition.getUrl(); + if (urlOpt.isPresent()) { + final URL url = urlOpt.get(); + final Function> validationFunction = + thingModelWithExtensionsAndImports -> + validateThingAttributes(thingModelWithExtensionsAndImports, attributes, resourcePath, + dittoHeaders); + return fetchResolveAndValidateWith(url, dittoHeaders, validationFunction); + } else { + return success(); + } + } else { + return success(); + } + } - if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled()) { - final Optional definitionIdentifier = feature.getDefinition() - .map(FeatureDefinition::getFirstIdentifier); - final Optional urlOpt = definitionIdentifier.flatMap(DefinitionIdentifier::getUrl); + @Override + public CompletionStage validateThingAttributes(final ThingModel thingModel, + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled() && + attributes != null) { + return thingModelValidation.validateThingAttributes(thingModel, attributes, resourcePath, dittoHeaders); + } else { + return success(); + } + } + + @Override + public CompletionStage validateThingAttribute(@Nullable final ThingDefinition thingDefinition, + final JsonPointer attributePointer, + final JsonValue attributeValue, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled() && + thingDefinition != null) { + final Optional urlOpt = thingDefinition.getUrl(); + if (urlOpt.isPresent()) { + final URL url = urlOpt.get(); + final Function> validationFunction = + thingModelWithExtensionsAndImports -> + thingModelValidation.validateThingAttribute(thingModelWithExtensionsAndImports, + attributePointer, attributeValue, resourcePath, dittoHeaders); + return fetchResolveAndValidateWith(url, dittoHeaders, validationFunction); + } else { + return success(); + } + } else { + return success(); + } + } + + @Override + public CompletionStage validateFeatures(@Nullable final ThingDefinition thingDefinition, + final Features features, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled() && + thingDefinition != null) { + final Optional urlOpt = thingDefinition.getUrl(); if (urlOpt.isPresent()) { final URL url = urlOpt.get(); final Function> validationFunction = thingModelWithExtensionsAndImports -> - validateFeature(thingModelWithExtensionsAndImports, feature, dittoHeaders); + validateFeatures(thingModelWithExtensionsAndImports, features, resourcePath, + dittoHeaders); return fetchResolveAndValidateWith(url, dittoHeaders, validationFunction); } else { - return CompletableFuture.completedStage(null); + return success(); } } else { - return CompletableFuture.completedStage(null); + return success(); } } @Override - public CompletionStage validateFeature(final ThingModel thingModel, final Feature feature, - final DittoHeaders dittoHeaders) { + public CompletionStage validateFeatures(final ThingModel thingModel, + final Features features, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled()) { + return thingModelResolver.resolveThingModelSubmodels(thingModel, dittoHeaders) + .thenCompose(subModels -> + doValidateFeatures(subModels, features, resourcePath, dittoHeaders) + ); + } else { + return success(); + } + } + @Override + public CompletionStage validateFeature(final @Nullable ThingDefinition thingDefinition, + final Feature feature, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled()) { - return thingModelValidation.validateFeatureProperties(thingModel, feature, dittoHeaders); + final Optional thingModelUrlOpt = Optional.ofNullable(thingDefinition) + .flatMap(ThingDefinition::getUrl); + if (thingModelUrlOpt.isPresent()) { + final URL thingModelUrl = thingModelUrlOpt.get(); + final Function> validationFunction = + thingModelWithExtensionsAndImports -> + doValidateFeature(thingModelWithExtensionsAndImports, feature, resourcePath, + dittoHeaders); + return fetchResolveAndValidateWith(thingModelUrl, dittoHeaders, validationFunction); + } else { + return doValidateFeature(null, feature, resourcePath, dittoHeaders); + } + } else { - return CompletableFuture.completedStage(null); + return success(); } } + @Override + public CompletionStage validateFeature(@Nullable final ThingModel thingModel, + @Nullable final ThingModel featureThingModel, + final Feature feature, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (FeatureToggle.isWotIntegrationFeatureEnabled() && wotConfig.getValidationConfig().isEnabled()) { + if (thingModel != null && featureThingModel != null) { + return thingModelResolver.resolveThingModelSubmodels(thingModel, dittoHeaders) + .thenCompose(subModels -> + thingModelValidation.validateFeaturePresence( + reduceSubmodelMapKeyToFeatureId(subModels), + feature, + dittoHeaders + ).thenCompose(aVoid -> + thingModelValidation.validateFeatureProperties(featureThingModel, feature, + resourcePath, dittoHeaders) + ) + ); + } else if (thingModel != null) { + return thingModelResolver.resolveThingModelSubmodels(thingModel, dittoHeaders) + .thenCompose(subModels -> + thingModelValidation.validateFeaturePresence( + reduceSubmodelMapKeyToFeatureId(subModels), + feature, + dittoHeaders + ) + ); + } else { + return thingModelValidation.validateFeatureProperties(featureThingModel, feature, resourcePath, + dittoHeaders); + } + } else { + return success(); + } + } + + private static CompletionStage success() { + return CompletableFuture.completedStage(null); + } + private CompletionStage fetchResolveAndValidateWith(final URL url, final DittoHeaders dittoHeaders, - final Function> validationFunction) { - + final Function> validationFunction + ) { return thingModelResolver.resolveThingModel(url, dittoHeaders) .thenComposeAsync(validationFunction, executor); } + + private CompletionStage doValidateFeatures(final Map subModels, + @Nullable final Features features, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final Map featureThingModels = reduceSubmodelMapKeyToFeatureId(subModels); + return thingModelValidation.validateFeaturesPresence(featureThingModels, features, dittoHeaders) + .thenCompose(aVoid2 -> + thingModelValidation + .validateFeaturesProperties(featureThingModels, features, resourcePath, dittoHeaders) + ); + } + + private CompletionStage doValidateFeature(@Nullable final ThingModel thingModel, + final Feature feature, + final JsonPointer resourcePath, + 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(); + final Function> validationFunction = + featureThingModelWithExtensionsAndImports -> + validateFeature(thingModel, featureThingModelWithExtensionsAndImports, feature, + resourcePath, dittoHeaders); + return fetchResolveAndValidateWith(url, dittoHeaders, validationFunction); + } else { + return validateFeature(thingModel, null, feature, resourcePath, dittoHeaders); + } + } + + 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 index 009e395358..e4ef5a975f 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/WotThingModelValidator.java @@ -15,9 +15,16 @@ 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.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; @@ -31,22 +38,90 @@ public interface WotThingModelValidator { /** * TODO TJ doc */ - CompletionStage validateThing(Thing thing, DittoHeaders dittoHeaders); + CompletionStage validateThing(Thing thing, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + */ + CompletionStage validateThing(ThingModel thingModel, + Thing thing, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + */ + CompletionStage validateThingAttributes(@Nullable ThingDefinition thingDefinition, + @Nullable Attributes attributes, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + */ + CompletionStage validateThingAttributes(ThingModel thingModel, + @Nullable Attributes attributes, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + * @param thingDefinition + * @param attributePointer + * @param attributeValue + * @param resourcePath + * @param dittoHeaders + * @return + */ + CompletionStage validateThingAttribute(@Nullable ThingDefinition thingDefinition, + JsonPointer attributePointer, + JsonValue attributeValue, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + */ + CompletionStage validateFeatures(@Nullable ThingDefinition thingDefinition, + Features features, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); /** * TODO TJ doc */ - CompletionStage validateThing(ThingModel thingModel, Thing thing, DittoHeaders dittoHeaders); + CompletionStage validateFeatures(ThingModel thingModel, + Features features, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); /** * TODO TJ doc */ - CompletionStage validateFeature(Feature feature, DittoHeaders dittoHeaders); + CompletionStage validateFeature(@Nullable ThingDefinition thingDefinition, + Feature feature, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); /** * TODO TJ doc */ - CompletionStage validateFeature(ThingModel thingModel, Feature feature, DittoHeaders dittoHeaders); + CompletionStage validateFeature(@Nullable ThingModel thingModel, + @Nullable ThingModel featureThingModel, + Feature feature, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); /** * Creates a new instance of WotThingModelValidator with the given {@code wotConfig}. diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DittoWotExtension.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/DittoWotExtension.java similarity index 78% rename from wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DittoWotExtension.java rename to wot/model/src/main/java/org/eclipse/ditto/wot/model/DittoWotExtension.java index 68a9fcfef5..0eead261e1 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/generator/DittoWotExtension.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/DittoWotExtension.java @@ -10,9 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.wot.api.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/validation/src/main/java/org/eclipse/ditto/wot/validation/DefaultWotThingModelValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/DefaultWotThingModelValidation.java index 9d785867fd..3a482d4ef9 100644 --- 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 @@ -23,6 +23,9 @@ 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 org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.json.JsonKey; @@ -33,9 +36,10 @@ 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.DittoWotExtension; import org.eclipse.ditto.wot.model.Properties; import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.SingleUriAtContext; import org.eclipse.ditto.wot.model.ThingModel; import org.eclipse.ditto.wot.model.TmOptionalElement; import org.eclipse.ditto.wot.validation.config.TmValidationConfig; @@ -48,11 +52,10 @@ final class DefaultWotThingModelValidation implements WotThingModelValidation { private static final String ATTRIBUTES = "attributes"; + private static final String FEATURES = "features"; private static final String PROPERTIES = "properties"; private static final String DESIRED_PROPERTIES = "desiredProperties"; - private static final String DITTO_CATEGORY = "ditto:category"; - private final TmValidationConfig validationConfig; private final JsonSchemaTools jsonSchemaTools; @@ -63,41 +66,206 @@ public DefaultWotThingModelValidation(final TmValidationConfig validationConfig) @Override public CompletionStage validateThingAttributes(final ThingModel thingModel, - final Thing thing, - final DittoHeaders dittoHeaders) { + @Nullable final Attributes attributes, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + if (validationConfig.getThingValidationConfig().isEnforceAttributes() && attributes != null) { + return enforceThingAttributes(thingModel, attributes, resourcePath, dittoHeaders); + } + return success(); + } + @Override + public CompletionStage validateThingAttribute(final ThingModel thingModel, + final JsonPointer attributePointer, + final JsonValue attributeValue, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { if (validationConfig.getThingValidationConfig().isEnforceAttributes()) { - return enforceThingAttributes(thingModel, thing, dittoHeaders); + return enforceThingAttribute(thingModel, attributePointer, attributeValue, dittoHeaders); } return success(); } + @Override + public CompletionStage validateFeaturesPresence(final Map featureThingModels, + @Nullable final Features features, + final DittoHeaders dittoHeaders + ) { + final Set definedFeatureIds = featureThingModels.keySet(); + final Set existingFeatures = Optional.ofNullable(features) + .map(Features::stream) + .orElseGet(Stream::empty) + .map(Feature::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + final CompletableFuture firstStage; + if (validationConfig.getFeatureValidationConfig().isEnforcePresenceOfModeledFeatures()) { + if (!existingFeatures.containsAll(definedFeatureIds)) { + final LinkedHashSet 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); + firstStage = CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(dittoHeaders) + .build()); + } else { + firstStage = success(); + } + } else { + firstStage = success(); + } + + final CompletableFuture secondStage; + if (!validationConfig.getFeatureValidationConfig().isAllowNonModeledFeatures()) { + final LinkedHashSet extraFeatureIds = new LinkedHashSet<>(existingFeatures); + extraFeatureIds.removeAll(definedFeatureIds); + if (!extraFeatureIds.isEmpty()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Attempting to update the Thing with feature(s) are were not " + + "defined in the model: " + extraFeatureIds); + secondStage = CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(dittoHeaders) + .build()); + } else { + secondStage = success(); + } + } else { + secondStage = success(); + } + return firstStage.thenCompose(unused -> secondStage); + } + + @Override + public CompletionStage validateFeaturesProperties(final Map featureThingModels, + final @Nullable Features features, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final CompletableFuture> enforcedPropertiesListFuture; + if (validationConfig.getFeatureValidationConfig().isEnforceProperties() && features != null) { + final List> enforcedPropertiesFutures = featureThingModels + .entrySet() + .stream() + .filter(entry -> features.getFeature(entry.getKey()).isPresent()) + .map(entry -> + enforceFeatureProperties(entry.getValue(), + features.getFeature(entry.getKey()).orElseThrow(), + true, false, + resourcePath, dittoHeaders + ) + ) + .toList(); + enforcedPropertiesListFuture = + CompletableFuture.allOf(enforcedPropertiesFutures.toArray(new CompletableFuture[0])) + .thenApply(ignored -> enforcedPropertiesFutures.stream() + .map(CompletableFuture::join) + .toList() + ); + } else { + enforcedPropertiesListFuture = success(); + } + + if (validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties() && features != null) { + final List> enforcedDesiredPropertiesFutures = featureThingModels + .entrySet() + .stream() + .map(entry -> enforceFeatureProperties(entry.getValue(), + features.getFeature(entry.getKey()).orElseThrow(), + false, true, resourcePath, dittoHeaders) + ) + .toList(); + return enforcedPropertiesListFuture.thenCompose(voidL -> + CompletableFuture.allOf(enforcedDesiredPropertiesFutures.toArray(new CompletableFuture[0])) + .thenApply(ignored -> enforcedDesiredPropertiesFutures.stream() + .map(CompletableFuture::join) + .toList() + ) + ).thenApply(voidL -> null); + } + return enforcedPropertiesListFuture.thenApply(voidL -> null); + } + + @Override + public CompletionStage validateFeaturePresence(final Map featureThingModels, + final Feature feature, + final DittoHeaders dittoHeaders + ) { + final Set definedFeatureIds = featureThingModels.keySet(); + final String featureId = feature.getId(); + + final CompletableFuture stage; + if (!validationConfig.getFeatureValidationConfig().isAllowNonModeledFeatures()) { + if (!definedFeatureIds.contains(featureId)) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("Attempting to update the Thing with a feature which is not " + + "defined in the model: <" + featureId + ">"); + stage = CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(dittoHeaders) + .build()); + } else { + stage = success(); + } + } else { + stage = success(); + } + return stage; + } + + @Override + public CompletionStage validateFeatureProperties(final ThingModel featureThingModel, + final Feature feature, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + final CompletableFuture enforcedPropertiesFuture; + if (validationConfig.getFeatureValidationConfig().isEnforceProperties()) { + enforcedPropertiesFuture = enforceFeatureProperties(featureThingModel, + feature, true, false, resourcePath, dittoHeaders); + } else { + enforcedPropertiesFuture = success(); + } + + if (validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties()) { + return enforcedPropertiesFuture.thenCompose(aVoid -> + enforceFeatureProperties(featureThingModel, + feature, false, true, resourcePath, dittoHeaders) + ); + } + return enforcedPropertiesFuture; + } + private CompletableFuture enforceThingAttributes(final ThingModel thingModel, - final Thing thing, - final DittoHeaders dittoHeaders) { + final Attributes attributes, + final JsonPointer resourcePath, + final DittoHeaders dittoHeaders + ) { + + final JsonPointer attributesPointer = JsonPointer.of(ATTRIBUTES); + final JsonPointer pathPrefix = attributesPointer.equals(resourcePath) ? JsonPointer.empty() : attributesPointer; + return thingModel.getProperties() .map(tdProperties -> { - final Attributes attributes = - thing.getAttributes().orElseGet(() -> Attributes.newBuilder().build()); - final String containerNamePlural = "Thing's attributes"; final CompletableFuture ensureRequiredPropertiesStage = ensureRequiredProperties(thingModel, dittoHeaders, tdProperties, attributes, containerNamePlural, "Thing's attribute", - JsonPointer.of(ATTRIBUTES), false); + pathPrefix, false); final CompletableFuture ensureOnlyDefinedPropertiesStage; if (!validationConfig.getThingValidationConfig().isAllowNonModeledAttributes()) { - ensureOnlyDefinedPropertiesStage = - ensureOnlyDefinedProperties(dittoHeaders, tdProperties, attributes, containerNamePlural, - false); + ensureOnlyDefinedPropertiesStage = ensureOnlyDefinedProperties(thingModel, dittoHeaders, + tdProperties, attributes, containerNamePlural, false); } else { ensureOnlyDefinedPropertiesStage = success(); } final CompletableFuture validatePropertiesStage = - getValidatePropertiesStage(dittoHeaders, tdProperties, attributes, - containerNamePlural, JsonPointer.of(ATTRIBUTES), false); + getValidatePropertiesStage(thingModel, dittoHeaders, tdProperties, attributes, + containerNamePlural, pathPrefix, false); return CompletableFuture.allOf( ensureRequiredPropertiesStage, @@ -107,6 +275,34 @@ private CompletableFuture enforceThingAttributes(final ThingModel thingMod }).orElseGet(DefaultWotThingModelValidation::success); } + private CompletableFuture enforceThingAttribute(final ThingModel thingModel, + final JsonPointer attributePath, + final JsonValue attributeValue, + final DittoHeaders dittoHeaders + ) { + + return thingModel.getProperties() + .map(tdProperties -> { + final Attributes attributes = Attributes.newBuilder().set(attributePath, attributeValue).build(); + final CompletableFuture ensureOnlyDefinedPropertiesStage; + if (!validationConfig.getThingValidationConfig().isAllowNonModeledAttributes()) { + ensureOnlyDefinedPropertiesStage = ensureOnlyDefinedProperties(thingModel, dittoHeaders, + tdProperties, attributes, "Thing's attributes", false); + } else { + ensureOnlyDefinedPropertiesStage = success(); + } + + final CompletableFuture validatePropertiesStage = + getValidatePropertyStage(dittoHeaders, tdProperties, attributePath, attributeValue, + "Thing's attribute <" + attributePath + ">"); + + return CompletableFuture.allOf( + ensureOnlyDefinedPropertiesStage, + validatePropertiesStage + ); + }).orElseGet(DefaultWotThingModelValidation::success); + } + private CompletableFuture ensureRequiredProperties(final ThingModel thingModel, final DittoHeaders dittoHeaders, final Properties tdProperties, @@ -116,26 +312,24 @@ private CompletableFuture ensureRequiredProperties(final ThingModel thingM final JsonPointer pointerPrefix, final boolean handleDittoCategory ) { - final Map nonProvidedRequiredProperties = filterNonProvidedRequiredProperties(tdProperties, thingModel, propertiesContainer, handleDittoCategory); final CompletableFuture requiredPropertiesStage; if (!nonProvidedRequiredProperties.isEmpty()) { final var exceptionBuilder = WotThingModelPayloadValidationException - .newBuilder("Required properties were missing from the " + containerNamePlural); + .newBuilder("Required JSON fields were missing from the " + containerNamePlural); nonProvidedRequiredProperties.forEach((rpKey, requiredProperty) -> { JsonPointer fullPointer = pointerPrefix; - if (handleDittoCategory && requiredProperty.contains(DITTO_CATEGORY)) { - fullPointer = fullPointer.addLeaf( - JsonKey.of(requiredProperty.getValue(DITTO_CATEGORY).orElseThrow().asString()) - ); + 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 present") + List.of(containerName + " <" + rpKey + "> is non optional and must be provided") ); } ); @@ -147,19 +341,28 @@ private CompletableFuture ensureRequiredProperties(final ThingModel thingM return requiredPropertiesStage; } + private 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); + } + private Map filterNonProvidedRequiredProperties(final Properties tdProperties, final ThingModel thingModel, final JsonObject propertiesContainer, final boolean handleDittoCategory ) { - final Map requiredProperties = extractRequiredProperties(tdProperties, thingModel); final Map nonProvidedRequiredProperties = new LinkedHashMap<>(requiredProperties); if (handleDittoCategory) { requiredProperties.forEach((rpKey, requiredProperty) -> { - final Optional dittoCategory = requiredProperty.getValue(DITTO_CATEGORY); + final Optional dittoCategory = determineDittoCategory(thingModel, requiredProperty); if (dittoCategory.isPresent()) { - propertiesContainer.getValue(dittoCategory.get().asString()) + propertiesContainer.getValue(dittoCategory.get()) .filter(JsonValue::isObject) .map(JsonValue::asObject) .ifPresent(categorizedProperties -> categorizedProperties.getKeys().stream() @@ -180,22 +383,20 @@ private Map filterNonProvidedRequiredProperties(final Properti return nonProvidedRequiredProperties; } - private CompletableFuture ensureOnlyDefinedProperties(final DittoHeaders dittoHeaders, + private CompletableFuture ensureOnlyDefinedProperties(final ThingModel thingModel, + final DittoHeaders dittoHeaders, final Properties tdProperties, final JsonObject propertiesContainer, final String containerNamePlural, final boolean handleDittoCategory ) { - final Set allDefinedPropertyKeys = tdProperties.keySet(); final Set allAvailablePropertiesKeys = propertiesContainer.getKeys().stream().map(JsonKey::toString) .collect(Collectors.toCollection(LinkedHashSet::new)); if (handleDittoCategory) { tdProperties.forEach((propertyName, property) -> { - final Optional dittoCategory = property.getValue(DITTO_CATEGORY) - .filter(JsonValue::isString) - .map(JsonValue::asString); + final Optional dittoCategory = determineDittoCategory(thingModel, property); final String categorizedPropertyName = dittoCategory .map(c -> c + "/").orElse("") .concat(propertyName); @@ -211,7 +412,7 @@ private CompletableFuture ensureOnlyDefinedProperties(final DittoHeaders d if (!allAvailablePropertiesKeys.isEmpty()) { final var exceptionBuilder = WotThingModelPayloadValidationException .newBuilder("The " + containerNamePlural + " contained " + - "keys which were not defined in the model: " + allAvailablePropertiesKeys); + "JSON fields which were not defined in the model: " + allAvailablePropertiesKeys); return CompletableFuture.failedFuture(exceptionBuilder .dittoHeaders(dittoHeaders) .build()); @@ -219,24 +420,24 @@ private CompletableFuture ensureOnlyDefinedProperties(final DittoHeaders d return success(); } - private CompletableFuture getValidatePropertiesStage(final DittoHeaders dittoHeaders, + private CompletableFuture getValidatePropertiesStage(final ThingModel thingModel, + final DittoHeaders dittoHeaders, final Properties tdProperties, final JsonObject propertiesContainer, final String containerNamePlural, final JsonPointer pointerPrefix, final boolean handleDittoCategory ) { - final CompletableFuture validatePropertiesStage; final Map invalidProperties; if (handleDittoCategory) { invalidProperties = determineInvalidProperties(tdProperties, - p -> propertiesContainer.getValue(p.getValue(DITTO_CATEGORY) - .filter(JsonValue::isString) - .map(JsonValue::asString) - .map(c -> c + "/") - .orElse("") - .concat(p.getPropertyName())), + p -> propertiesContainer.getValue( + determineDittoCategory(thingModel, p) + .map(c -> c + "/") + .orElse("") + .concat(p.getPropertyName()) + ), dittoHeaders ); } else { @@ -250,17 +451,16 @@ private CompletableFuture getValidatePropertiesStage(final DittoHeaders di final var exceptionBuilder = WotThingModelPayloadValidationException .newBuilder("The " + containerNamePlural + " contained validation errors, " + "check the validation details."); - invalidProperties.forEach((key, value) -> { + invalidProperties.forEach((property, validationOutputUnit) -> { JsonPointer fullPointer = pointerPrefix; - if (handleDittoCategory && key.contains(DITTO_CATEGORY)) { - fullPointer = fullPointer.addLeaf( - JsonKey.of(key.getValue(DITTO_CATEGORY).orElseThrow().asString()) - ); + final Optional dittoCategory = determineDittoCategory(thingModel, property); + if (handleDittoCategory && dittoCategory.isPresent()) { + fullPointer = fullPointer.addLeaf(JsonKey.of(dittoCategory.get())); } - fullPointer = fullPointer.addLeaf(JsonKey.of(key.getPropertyName())); + fullPointer = fullPointer.addLeaf(JsonKey.of(property.getPropertyName())); exceptionBuilder.addValidationDetail( fullPointer, - value.getDetails().stream() + validationOutputUnit.getDetails().stream() .map(ou -> ou.getInstanceLocation() + ": " + ou.getErrors()) .toList() ); @@ -274,9 +474,57 @@ private CompletableFuture getValidatePropertiesStage(final DittoHeaders di return validatePropertiesStage; } - private Map determineInvalidProperties(final Properties tdProperties, - final Function> propertyExtractor, final DittoHeaders dittoHeaders) { + private CompletableFuture getValidatePropertyStage(final DittoHeaders dittoHeaders, + final Properties tdProperties, + final JsonPointer attributePath, + final JsonValue propertyValue, + final String propertyDescription + ) { + final JsonValue valueToValidate; + if (attributePath.getLevelCount() > 1) { + valueToValidate = JsonObject.newBuilder() + .set(attributePath.getSubPointer(1).orElseThrow(), propertyValue) + .build(); + } else { + valueToValidate = propertyValue; + } + + final Optional validationOutput = tdProperties + .getProperty(attributePath.getRoot().orElseThrow()) + .map(property -> + jsonSchemaTools.validateDittoJsonBasedOnDataSchema( + property, + attributePath, + valueToValidate, + dittoHeaders + ) + ) + .filter(outputUnit -> !outputUnit.isValid()); + + final CompletableFuture validatePropertiesStage; + if (validationOutput.isPresent()) { + final var exceptionBuilder = WotThingModelPayloadValidationException + .newBuilder("The " + propertyDescription + " contained validation errors, " + + "check the validation details."); + exceptionBuilder.addValidationDetail( + JsonPointer.empty(), + validationOutput.get().getDetails().stream() + .map(ou -> ou.getInstanceLocation() + ": " + ou.getErrors()) + .toList() + ); + validatePropertiesStage = CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(dittoHeaders) + .build()); + } else { + validatePropertiesStage = success(); + } + return validatePropertiesStage; + } + private Map determineInvalidProperties(final Properties tdProperties, + final Function> propertyExtractor, + final DittoHeaders dittoHeaders + ) { return tdProperties.entrySet().stream() .flatMap(tdPropertyEntry -> propertyExtractor.apply(tdPropertyEntry.getValue()) @@ -284,6 +532,7 @@ private Map determineInvalidProperties(final Properties td tdPropertyEntry.getValue(), jsonSchemaTools.validateDittoJsonBasedOnDataSchema( tdPropertyEntry.getValue(), + JsonPointer.empty(), attributeValue, dittoHeaders ) @@ -293,81 +542,16 @@ private Map determineInvalidProperties(final Properties td ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - @Override - public CompletionStage validateFeaturesProperties(final Map featureThingModels, - final Features features, - final DittoHeaders dittoHeaders) { - - // TODO TJ implement - this should collect errors of all invalid features of the thing in a combined exception! - final CompletableFuture> enforcedPropertiesListFuture; - if (validationConfig.getFeatureValidationConfig().isEnforceProperties()) { - final List> enforcedPropertiesFutures = featureThingModels - .entrySet() - .stream() - .filter(entry -> features.getFeature(entry.getKey()).isPresent()) - .map(entry -> - enforceFeatureProperties(entry.getValue(), - features.getFeature(entry.getKey()).orElseThrow(), true, false, dittoHeaders) - ) - .toList(); - enforcedPropertiesListFuture = - CompletableFuture.allOf(enforcedPropertiesFutures.toArray(new CompletableFuture[0])) - .thenApply(ignored -> enforcedPropertiesFutures.stream() - .map(CompletableFuture::join) - .toList() - ); - } else { - enforcedPropertiesListFuture = CompletableFuture.completedFuture(null); - } - - if (validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties()) { - final List> enforcedDesiredPropertiesFutures = featureThingModels - .entrySet() - .stream() - .map(entry -> - enforceFeatureProperties(entry.getValue(), - features.getFeature(entry.getKey()).orElseThrow(), false, true, dittoHeaders) - ) - .toList(); - return enforcedPropertiesListFuture.thenCompose(voidL -> - CompletableFuture.allOf(enforcedDesiredPropertiesFutures.toArray(new CompletableFuture[0])) - .thenApply(ignored -> enforcedDesiredPropertiesFutures.stream() - .map(CompletableFuture::join) - .toList() - ) - ).thenApply(voidL -> null); - } - return enforcedPropertiesListFuture.thenApply(voidL -> null); - } - - @Override - public CompletionStage validateFeatureProperties(final ThingModel featureThingModel, - final Feature feature, - final DittoHeaders dittoHeaders) { - - final CompletableFuture enforcedPropertiesFuture; - if (validationConfig.getFeatureValidationConfig().isEnforceProperties()) { - enforcedPropertiesFuture = enforceFeatureProperties(featureThingModel, - feature, true, false, dittoHeaders); - } else { - enforcedPropertiesFuture = success(); - } - - if (validationConfig.getFeatureValidationConfig().isEnforceDesiredProperties()) { - return enforcedPropertiesFuture.thenCompose(aVoid -> - enforceFeatureProperties(featureThingModel, - feature, false, true, dittoHeaders) - ); - } - return enforcedPropertiesFuture; - } - private CompletableFuture enforceFeatureProperties(final ThingModel featureThingModel, final Feature feature, final boolean checkForRequiredProperties, final boolean desiredProperties, + final JsonPointer resourcePath, final DittoHeaders dittoHeaders ) { + final JsonPointer featuresPointer = JsonPointer.of(FEATURES); + final JsonPointer pathPrefix = featuresPointer.equals(resourcePath) ? JsonPointer.of(feature.getId()) : + featuresPointer.addLeaf(JsonKey.of(feature.getId())); return featureThingModel.getProperties() .map(tdProperties -> { @@ -383,7 +567,8 @@ private CompletableFuture enforceFeatureProperties(final ThingModel featur final String containerNamePrefix = "Feature <" + feature.getId() + ">'s " + (desiredProperties ? "desired " : ""); final String containerNamePlural = containerNamePrefix + "properties"; - final String path = desiredProperties ? DESIRED_PROPERTIES : PROPERTIES; + final JsonPointer path = desiredProperties ? pathPrefix.addLeaf(JsonKey.of(DESIRED_PROPERTIES)) : + pathPrefix.addLeaf(JsonKey.of(PROPERTIES)); final CompletableFuture ensureRequiredPropertiesStage; if (checkForRequiredProperties) { @@ -394,18 +579,16 @@ private CompletableFuture enforceFeatureProperties(final ThingModel featur ensureRequiredPropertiesStage = success(); } - final CompletableFuture ensureOnlyDefinedPropertiesStage; if (!validationConfig.getThingValidationConfig().isAllowNonModeledAttributes()) { - ensureOnlyDefinedPropertiesStage = - ensureOnlyDefinedProperties(dittoHeaders, tdProperties, featureProperties, - containerNamePlural, true); + ensureOnlyDefinedPropertiesStage = ensureOnlyDefinedProperties(featureThingModel, dittoHeaders, + tdProperties, featureProperties, containerNamePlural, true); } else { - ensureOnlyDefinedPropertiesStage = CompletableFuture.completedFuture(null); + ensureOnlyDefinedPropertiesStage = success(); } final CompletableFuture validatePropertiesStage = - getValidatePropertiesStage(dittoHeaders, tdProperties, featureProperties, + getValidatePropertiesStage(featureThingModel, dittoHeaders, tdProperties, featureProperties, containerNamePlural, JsonPointer.of(path), true); return CompletableFuture.allOf( @@ -416,12 +599,13 @@ private CompletableFuture enforceFeatureProperties(final ThingModel featur }).orElseGet(DefaultWotThingModelValidation::success); } - private static CompletableFuture success() { + private static CompletableFuture success() { return CompletableFuture.completedFuture(null); } - private Map extractRequiredProperties(final Properties tdProperties, - final ThingModel thingModel) { + private static Map extractRequiredProperties(final Properties tdProperties, + final ThingModel thingModel + ) { return thingModel.getTmOptional().map(tmOptionalElements -> { final Map allRequiredProperties = new LinkedHashMap<>(tdProperties); tmOptionalElements.stream() 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 index ab22077cc8..8778c283a7 100644 --- 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 @@ -13,11 +13,14 @@ package org.eclipse.ditto.wot.validation; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; 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.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.wot.model.SingleDataSchema; import org.eclipse.ditto.wot.model.WotInternalErrorException; @@ -27,6 +30,7 @@ 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; @@ -83,15 +87,25 @@ JsonSchema extractFromSingleDataSchema(final SingleDataSchema dataSchema, final } OutputUnit validateDittoJsonBasedOnDataSchema(final SingleDataSchema dataSchema, + final JsonPointer pointerPath, final JsonValue jsonValue, - final DittoHeaders dittoHeaders) { + final DittoHeaders dittoHeaders + ) { final JsonSchema jsonSchema = extractFromSingleDataSchema(dataSchema, dittoHeaders); - return validateDittoJson(jsonSchema, jsonValue, dittoHeaders); + final JsonPointer relativePropertyPath; + if (pointerPath.getLevelCount() > 1) { + relativePropertyPath = pointerPath.getSubPointer(1).orElseThrow(); + } else { + relativePropertyPath = JsonPointer.empty(); + } + return validateDittoJson(jsonSchema, relativePropertyPath, jsonValue, dittoHeaders); } OutputUnit validateDittoJson(final JsonSchema jsonSchema, + final JsonPointer relativePropertyPath, final JsonValue jsonValue, - final DittoHeaders dittoHeaders) { + final DittoHeaders dittoHeaders + ) { final JsonNode jsonNode; try { final byte[] bytes = cborFactory.toByteArray(jsonValue); @@ -109,6 +123,32 @@ OutputUnit validateDittoJson(final JsonSchema jsonSchema, .dittoHeaders(dittoHeaders) .build(); } - return jsonSchema.validate(jsonNode, OutputFormat.LIST); + 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()) { + if (detail.getInstanceLocation().equals(relativePropertyPath.toString())) { + validationDetails.remove(detail); + detail.setInstanceLocation(""); + validationDetails.add(detail); + } else { + validationDetails.remove(detail); + } + } + }); + validate.setDetails(validationDetails); + } + return validate; + } + + private static JsonNodePath transformJsonPointerToJsonNodePath(final JsonPointer pointerPath) { + JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); + JsonPointer currentPointer = pointerPath; + while (!currentPointer.isEmpty()) { + fragment = fragment.append(currentPointer.getRoot().orElseThrow().toString()); + currentPointer = currentPointer.nextLevel(); + } + return fragment; } } 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 index 747e8e8277..c611a1e2f5 100644 --- 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 @@ -15,7 +15,12 @@ import java.util.Map; import java.util.concurrent.CompletionStage; +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.Features; import org.eclipse.ditto.things.model.Thing; @@ -34,22 +39,60 @@ public interface WotThingModelValidation { * TODO TJ doc */ CompletionStage validateThingAttributes(ThingModel thingModel, - Thing thing, - DittoHeaders dittoHeaders); + @Nullable Attributes attributes, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + * @param thingModel + * @param attributePointer + * @param attributeValue + * @param resourcePath + * @param dittoHeaders + * @return + */ + CompletionStage validateThingAttribute(ThingModel thingModel, + JsonPointer attributePointer, + JsonValue attributeValue, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + */ + CompletionStage validateFeaturesPresence(Map featureThingModels, + @Nullable Features features, + DittoHeaders dittoHeaders + ); /** * TODO TJ doc */ CompletionStage validateFeaturesProperties(Map featureThingModels, - Features features, - DittoHeaders dittoHeaders); + @Nullable Features features, + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); + + /** + * TODO TJ doc + */ + CompletionStage validateFeaturePresence(Map featureThingModels, + Feature feature, + DittoHeaders dittoHeaders + ); /** * TODO TJ doc */ CompletionStage validateFeatureProperties(ThingModel featureThingModel, Feature feature, - DittoHeaders dittoHeaders); + JsonPointer resourcePath, + DittoHeaders dittoHeaders + ); /** * TODO TJ doc 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 index d1a3f34a1d..13d6d933b1 100644 --- 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 @@ -24,6 +24,18 @@ @Immutable public interface FeatureValidationConfig { + /** + * TODO TJ doc + * @return + */ + boolean isEnforcePresenceOfModeledFeatures(); + + /** + * TODO TJ doc + * @return + */ + boolean isAllowNonModeledFeatures(); + /** * @return whether to enforce/validate properties of a feature following the defined WoT properties. */ @@ -71,6 +83,10 @@ public interface FeatureValidationConfig { */ enum ConfigValue implements KnownConfigValue { + ENFORCE_PRESENCE_OF_MODELED_FEATURES("enforce-presence-of-modeled-features", true), + + ALLOW_NON_MODELED_FEATURES("allow-non-modeled-features", false), + ENFORCE_PROPERTIES("enforce-properties", true), ALLOW_NON_MODELED_PROPERTIES("allow-non-modeled-properties", false),