Skip to content

Commit

Permalink
[#559] implement conditional requests based on the condition in the d…
Browse files Browse the repository at this point in the history
…itto headers;

add AbstractConditionCheckingCommandStrategy which checks the specified condition against the actual thing state;
add new exception ThingConditionFailedException;

Signed-off-by: Stefan Maute <stefan.maute@bosch.io>
  • Loading branch information
Stefan Maute committed Aug 18, 2021
1 parent d7bdc11 commit bf49105
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ public S journalTags(final Collection<String> journalTags) {

@Override
public S condition(final Condition condition) {
putCharSequence(DittoHeaderDefinition.CONDITION, condition.toString());
putCharSequence(DittoHeaderDefinition.CONDITION, condition.getRqlCondition());
return myself;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ private SendingOrDropped publishToGenericTarget(final ExpressionResolver resolve
result = new Sending(sendingContext.setExternalMessage(mappedMessage), responsesFuture,
connectionIdResolver, l);
} else {
l.debug("Signal dropped, target address unresolved: {0}", address);
l.debug("Signal dropped, target address unresolved: <{}>", address);
result = new Dropped(sendingContext, "Signal dropped, target address unresolved: {0}");
}
return result;
Expand Down
9 changes: 9 additions & 0 deletions internal/utils/persistent-actors/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
<artifactId>ditto-internal-utils-akka</artifactId>
</dependency>

<dependency>
<groupId>org.eclipse.ditto</groupId>
<artifactId>ditto-rql-query</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.ditto</groupId>
<artifactId>ditto-rql-parser</artifactId>
</dependency>

<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.version}</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.internal.utils.persistentactors.condition;

import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;

import java.util.function.Predicate;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.entity.Entity;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.base.model.signals.events.Event;
import org.eclipse.ditto.internal.utils.persistentactors.etags.AbstractConditionHeaderCheckingCommandStrategy;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.rql.parser.RqlPredicateParser;
import org.eclipse.ditto.rql.query.criteria.Criteria;
import org.eclipse.ditto.rql.query.filter.QueryFilterCriteriaFactory;
import org.eclipse.ditto.rql.query.things.ThingPredicateVisitor;
import org.eclipse.ditto.things.model.Thing;
import org.eclipse.ditto.things.model.signals.commands.ThingCommand;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException;
import org.eclipse.ditto.things.model.signals.commands.modify.CreateThing;

/**
* Responsible to check conditional requests based on the thing's current state and the specified condition header.
*
* @param <C> the type of the handled commands
* @param <S> the type of the addressed entity
* @param <K> the type of the context
* @param <E> the type of the emitted events
*/
@Immutable
public abstract class AbstractConditionCheckingCommandStrategy<
C extends Command<?>,
S extends Entity<?>,
K,
E extends Event<?>> extends AbstractConditionHeaderCheckingCommandStrategy<C, S, K, E> {

/**
* Construct a command-strategy with condition header checking.
*
* @param theMatchingClass final class of the command to handle.
*/
protected AbstractConditionCheckingCommandStrategy(final Class<C> theMatchingClass) {
super(theMatchingClass);
}

/**
* Checks condition header on the (sub-)entity determined by the given {@code command} and {@code thing}.
*
* @param context the context.
* @param entity the entity, may be {@code null}.
* @param nextRevision the next revision number of the entity.
* @param command the command which addresses either the whole entity or a sub-entity
* @return Either and error result if the specified condition does not meet the condition or the result of the
* extending strategy.
*/
@Override
public Result<E> apply(final Context<K> context, @Nullable final S entity, final long nextRevision,
final C command) {

final String condition = command.getDittoHeaders().getCondition().orElse(null);

if (condition != null && entity != null) {
context.getLog().withCorrelationId(command)
.debug("Validating condition <{}> on command <{}>.", condition, command);

try {
checkCondition((Thing) entity, command, condition);
context.getLog().withCorrelationId(command)
.debug("Validating condition succeeded.");
} catch (final DittoRuntimeException dre) {
context.getLog().withCorrelationId(command)
.debug("Validating condition failed with exception <{}>.", dre.getMessage());
return ResultFactory.newErrorResult(dre, command);
}
}

return super.apply(context, entity, nextRevision, command);
}

@Override
public boolean isDefined(final Context<K> context, @Nullable final S entity, final C command) {
checkNotNull(context, "Context");
checkNotNull(command, "Command");

return !(command instanceof CreateThing) && command instanceof ThingCommand && entity instanceof Thing;
}

private void checkCondition(final Thing entity, final C command, final String condition) {
final DittoHeaders dittoHeaders = command.getDittoHeaders();
final Criteria criteria = QueryFilterCriteriaFactory.modelBased(RqlPredicateParser.getInstance())
.filterCriteria(condition, dittoHeaders);
final Predicate<Thing> predicate = ThingPredicateVisitor.apply(criteria);
if (!predicate.test(entity)) {
throw ThingConditionFailedException.newBuilder(condition)
.dittoHeaders(dittoHeaders)
.build();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2021 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
*/

/**
* Entity-tag handlers.
*/
@org.eclipse.ditto.utils.jsr305.annotations.AllValuesAreNonnullByDefault
package org.eclipse.ditto.internal.utils.persistentactors.condition;
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
import org.eclipse.ditto.base.model.entity.id.WithEntityId;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.base.model.signals.events.Event;
import org.eclipse.ditto.internal.utils.headers.conditional.ConditionalHeadersValidator;
import org.eclipse.ditto.internal.utils.persistentactors.commands.AbstractCommandStrategy;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.base.model.signals.events.Event;

/**
* Responsible to check conditional (http) headers based on the thing's current eTag value.
Expand All @@ -48,7 +48,7 @@ public abstract class AbstractConditionHeaderCheckingCommandStrategy<
E extends Event<?>> extends AbstractCommandStrategy<C, S, K, E> implements ETagEntityProvider<C, S> {

/**
* Construct a command-strategy with condition header checking..
* Construct a command-strategy with condition header checking.
*
* @param theMatchingClass final class of the command to handle.
*/
Expand All @@ -63,7 +63,7 @@ protected AbstractConditionHeaderCheckingCommandStrategy(final Class<C> theMatch

/**
* Checks conditional headers on the (sub-)entity determined by the given {@code command} and {@code thing}.
* Currently supports only {@link org.eclipse.ditto.internal.utils.headers.conditional.IfMatchPreconditionHeader}
* Currently, supports only {@link org.eclipse.ditto.internal.utils.headers.conditional.IfMatchPreconditionHeader}
* and {@link org.eclipse.ditto.internal.utils.headers.conditional.IfNoneMatchPreconditionHeader}
*
* @param context the context.
Expand Down Expand Up @@ -114,4 +114,5 @@ private static <C extends Command<?>> boolean commandHasEntityIdAndIsEqual(Entit
return false;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.things.model.signals.commands.exceptions;

import java.net.URI;
import java.text.MessageFormat;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;

import org.eclipse.ditto.base.model.common.HttpStatus;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeExceptionBuilder;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.json.JsonParsableException;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.things.model.ThingException;

/**
* Thrown when validating a condition on a Thing or one of its sub-entities is failing.
*/
@Immutable
@JsonParsableException(errorCode = ThingConditionFailedException.ERROR_CODE)
public final class ThingConditionFailedException extends DittoRuntimeException implements ThingException {

/**
* Error code of this exception.
*/
public static final String ERROR_CODE = ERROR_CODE_PREFIX + "condition.failed";

private static final String MESSAGE_TEMPLATE =
"The specified condition ''{0}'' does not match the requested Thing.";

private static final String DEFAULT_DESCRIPTION = "The condition provided in the condition header " +
"evaluated to false for the requested Thing. Please check the value of your condition header value.";

private ThingConditionFailedException(final DittoHeaders dittoHeaders,
@Nullable final String message,
@Nullable final String description,
@Nullable final Throwable cause,
@Nullable final URI href) {
super(ERROR_CODE, HttpStatus.PRECONDITION_FAILED, dittoHeaders, message, description, cause, href);
}

/**
* A mutable builder for a {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException}.
*
* @param condition the condition to apply for the request.
* @return the builder.
*/
public static Builder newBuilder(final String condition) {
return new Builder(condition);
}

/**
* Constructs a new {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException}
* object with the exception message extracted from the given JSON object.
*
* @param jsonObject the JSON to read the {@link org.eclipse.ditto.base.model.exceptions.DittoRuntimeException.JsonFields#MESSAGE} field from.
* @param dittoHeaders the headers of the command which resulted in this exception.
* @return the new {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException}.
* @throws NullPointerException if any argument is {@code null}.
* @throws org.eclipse.ditto.json.JsonMissingFieldException if this JsonObject did not contain an error message.
* @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected
* format.
*/
public static ThingConditionFailedException fromJson(final JsonObject jsonObject,
final DittoHeaders dittoHeaders) {
return DittoRuntimeException.fromJson(jsonObject, dittoHeaders, new Builder());
}

@Override
public DittoRuntimeException setDittoHeaders(final DittoHeaders dittoHeaders) {
return new Builder()
.message(getMessage())
.description(getDescription().orElse(null))
.cause(getCause())
.href(getHref().orElse(null))
.dittoHeaders(dittoHeaders)
.build();
}

/**
* A mutable builder with a fluent API for a {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException}.
*/
@NotThreadSafe
public static final class Builder
extends DittoRuntimeExceptionBuilder<ThingConditionFailedException> {

private Builder() {
description(DEFAULT_DESCRIPTION);
}

private Builder(final String condition) {
this();
message(MessageFormat.format(MESSAGE_TEMPLATE, condition));
}

@Override
protected ThingConditionFailedException doBuild(final DittoHeaders dittoHeaders,
@Nullable final String message,
@Nullable final String description,
@Nullable final Throwable cause,
@Nullable final URI href) {
return new ThingConditionFailedException(dittoHeaders, message, description, cause, href);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
import java.util.Arrays;
import java.util.Collection;

import org.eclipse.ditto.base.model.auth.AuthorizationContext;
import org.eclipse.ditto.base.model.auth.AuthorizationSubject;
import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType;
import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonParseOptions;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.base.model.auth.AuthorizationContext;
import org.eclipse.ditto.base.model.auth.AuthorizationSubject;
import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType;
import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.policies.model.PolicyId;
import org.eclipse.ditto.things.model.Attributes;
import org.eclipse.ditto.things.model.FeatureDefinition;
Expand Down Expand Up @@ -62,6 +62,7 @@
import org.eclipse.ditto.things.model.signals.commands.exceptions.PolicyIdNotModifiableException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.PolicyInvalidException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.PolicyNotAllowedException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConflictException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingDefinitionNotAccessibleException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingIdNotExplicitlySettableException;
Expand Down Expand Up @@ -350,6 +351,14 @@ public static final class Thing {
public static final PolicyInvalidException POLICY_INVALID_EXCEPTION =
PolicyInvalidException.newBuilder(REQUIRED_THING_PERMISSIONS, THING_ID).build();

/**
* A known {@code ThingConditionFailedException}.
*/
public static final ThingConditionFailedException THING_CONDITION_FAILED_EXCEPTION =
ThingConditionFailedException
.newBuilder("eq(attributes/attr1,42)")
.build();

private Thing() {
throw new AssertionError();
}
Expand Down
Loading

0 comments on commit bf49105

Please sign in to comment.