Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate the subscription topic on client #1197

Merged
merged 16 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions client/src/main/java/io/spine/client/FilterMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2019, TeamDev. All rights reserved.
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.client;

import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import io.spine.annotation.GeneratedMixin;
import io.spine.base.EntityState;
import io.spine.base.Field;
import io.spine.base.FieldPath;
import io.spine.code.proto.FieldDeclaration;
import io.spine.type.TypeUrl;

import java.util.Optional;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.spine.code.proto.ColumnOption.isColumn;
import static io.spine.util.Exceptions.newIllegalArgumentException;

/**
* Extends the {@link Filter} with validation routines.
*/
@GeneratedMixin
interface FilterMixin extends FilterOrBuilder {

/**
* Obtains the target field.
*/
default Field field() {
FieldPath fieldPath = getFieldPath();
return Field.withPath(fieldPath);
}

/**
* Checks if the target field is present in the specified message type.
*/
default boolean fieldPresentIn(Descriptor message) {
checkNotNull(message);
Field field = field();
boolean result = field.presentIn(message);
return result;
}

/**
* Verifies that the target field is present in the passed message type.
armiol marked this conversation as resolved.
Show resolved Hide resolved
*
* @throws IllegalArgumentException
* if the field is not present in the passed message type
*/
default void checkFieldPresentIn(Descriptor message) {
checkNotNull(message);
if (!fieldPresentIn(message)) {
throw newIllegalArgumentException(
"The field with path `%s` is not present in the message type `%s`.",
field(), message.getFullName());
}
}

/**
* Checks if the target field is an entity column in the passed message type.
*/
default boolean fieldIsColumnIn(Descriptor message) {
checkNotNull(message);
Optional<FieldDescriptor> fieldDescriptor = field().findDescriptor(message);
if (!fieldDescriptor.isPresent()) {
return false;
}
FieldDeclaration declaration = new FieldDeclaration(fieldDescriptor.get());
boolean result = isColumn(declaration);
return result;
}

/**
* Verifies that the target field is an entity column in the passed message type.
*
* @throws IllegalArgumentException
* if the field is not an entity column in the passed message type
*/
default void checkFieldIsColumnIn(Descriptor message) {
checkNotNull(message);
checkFieldAtTopLevel();
if (!fieldIsColumnIn(message)) {
throw newIllegalArgumentException(
"The entity column `%s` is not found in entity state type `%s`. " +
"Please check the field exists and is marked with `(column)` option.",
field(), message.getFullName());
}
}

/**
* Checks if the target field is a top-level field.
*/
default boolean fieldAtTopLevel() {
return !field().isNested();
}

/**
* Verifies that the target field is a top-level field.
*
* @throws IllegalArgumentException
* if the field is not a top-level field
*/
default void checkFieldAtTopLevel() {
if (!fieldAtTopLevel()) {
throw newIllegalArgumentException(
"The entity filter contains a nested entity column `%s`. " +
"Nested entity columns are currently not supported.",
field()
);
}
}

/**
* Verifies that the filter can be applied to the given {@code target}.
*
* <p>Makes sure the field specified in the filter is a valid entity column or a message field
* in the type enclosed by the {@code target}.
*
* @throws IllegalArgumentException
* if the field is not present in the target type or doesn't satisfy the constraints
*/
default void checkCanApplyTo(Target target) {
checkNotNull(target);

TypeUrl targetType = target.typeUrl();

Class<Message> javaClass = targetType.getMessageClass();
Descriptor descriptor = targetType.toTypeName()
.messageDescriptor();
if (EntityState.class.isAssignableFrom(javaClass)) {
checkFieldIsColumnIn(descriptor);
} else {
checkFieldPresentIn(descriptor);
}
}
}
85 changes: 85 additions & 0 deletions client/src/main/java/io/spine/client/TargetMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2019, TeamDev. All rights reserved.
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.client;

import com.google.protobuf.Message;
import io.spine.annotation.GeneratedMixin;
import io.spine.base.EntityState;
import io.spine.base.EventMessage;
import io.spine.type.TypeUrl;

import java.util.Collection;

import static io.spine.util.Exceptions.newIllegalArgumentException;

/**
* Extends the {@link Target} with validation routines.
*/
@GeneratedMixin
interface TargetMixin extends TargetOrBuilder {

/**
* Returns the URL of the target type.
*/
default TypeUrl typeUrl() {
String type = getType();
return TypeUrl.parse(type);
}

/**
* Verifies that the target type is a valid type for querying.
*
* @throws IllegalArgumentException
* if the target type is not a valid type for querying
*/
default void checkTypeValid() {
String type = getType();
Class<Message> targetClass = TypeUrl.parse(type)
.getMessageClass();
boolean isEntityState = EntityState.class.isAssignableFrom(targetClass);
boolean isEventMessage = EventMessage.class.isAssignableFrom(targetClass);
if (!isEntityState && !isEventMessage) {
throw newIllegalArgumentException(
"The queried type should represent either an entity state or an event " +
"message. Got type `%s` instead.", targetClass.getCanonicalName());
}
}

/**
* Verifies that the target has valid type and filters.
*
* @throws IllegalArgumentException
* if either the target type is not a valid type for querying or the filters are
* invalid
* @see FilterMixin#checkCanApplyTo(Target)
*/
@SuppressWarnings("ClassReferencesSubclass") // OK for a proto mixin.
default void checkValid() {
checkTypeValid();
Target thisAsTarget = (Target) this;
getFilters()
.getFilterList()
.stream()
.map(CompositeFilter::getFilterList)
.flatMap(Collection::stream)
.forEach(filter -> filter.checkCanApplyTo(thisAsTarget));
}
}
4 changes: 4 additions & 0 deletions client/src/main/java/io/spine/client/TopicBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,14 @@ public final class TopicBuilder extends TargetBuilder<Topic, TopicBuilder> {
* configuration.
*
* @return a new {@link io.spine.client.Topic Topic}
* @throws IllegalArgumentException
* if the built {@link Target} instance is invalid, e.g. contains filters with
* non-existent fields
*/
@Override
public Topic build() {
Target target = buildTarget();
target.checkValid();
FieldMask mask = composeMask();
Topic topic = topicFactory.composeTopic(target, mask);
return topic;
Expand Down
7 changes: 4 additions & 3 deletions client/src/main/java/io/spine/client/TopicFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import org.checkerframework.checker.nullness.qual.Nullable;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.spine.client.Targets.composeTarget;
import static java.lang.String.format;

/**
Expand Down Expand Up @@ -77,8 +76,8 @@ public TopicBuilder select(Class<? extends Message> targetType) {
public Topic allOf(Class<? extends Message> targetType) {
checkNotNull(targetType);

Target target = composeTarget(targetType, null, null);
Topic result = forTarget(target);
TopicBuilder builder = new TopicBuilder(targetType, this);
Topic result = builder.build();
return result;
}

Expand Down Expand Up @@ -112,6 +111,8 @@ Topic composeTopic(Target target, @Nullable FieldMask fieldMask) {
* @param target
* a {@code Target} to create a topic for
* @return an instance of {@code Topic}
* @apiNote Assumes the passed target is {@linkplain TargetMixin#checkValid() valid} and
* doesn't do any additional checks.
*/
@Internal
public Topic forTarget(Target target) {
Expand Down
2 changes: 2 additions & 0 deletions client/src/main/proto/spine/client/filters.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import "spine/base/field_path.proto";
// Use Target to specify and narrow down the source for Topic and Query by the target type and
// various criteria.
message Target {
option (is).java_type = "io.spine.client.TargetMixin";

// The type of the entity or event of interest.
//
Expand Down Expand Up @@ -96,6 +97,7 @@ message CompositeFilter {

// A filter matching some value in the event message/entity state.
message Filter {
option (is).java_type = "io.spine.client.FilterMixin";

// The path to the field to be matched.
//
Expand Down
Loading