From 362e086261c54b9316f5ca443bdde775be0274b5 Mon Sep 17 00:00:00 2001 From: David Joos Date: Thu, 28 May 2020 09:16:41 +0200 Subject: [PATCH] Revert "Fixed tests, added tests." This reverts commit dba51aa4 Signed-off-by: David Joos --- .../json/JsonPointerInvalidException.java | 166 ++++ model/base/pom.xml | 88 ++ .../entity/id/DefaultNamespacedEntityId.java | 159 ++++ .../validation/AbstractPatternValidator.java | 57 ++ .../validation/AttributePatternValidator.java | 31 + .../validation/EntityIdPatternValidator.java | 93 ++ .../validation/FeaturePatternValidator.java | 31 + .../entity/validation/PatternValidator.java | 45 + .../base/entity/validation/RegexPatterns.java | 121 +++ .../validation/SubjectPatternValidator.java | 33 + .../id/DefaultNamespacedEntityIdTest.java | 272 ++++++ .../entity/validation/RegexPatternsTest.java | 178 ++++ .../DittoMessageSubjectValueValidator.java | 50 + .../messages/MessageHeaderDefinition.java | 188 ++++ ...eFunctionParameterResolverFactoryTest.java | 186 ++++ .../ditto/model/policies/ImmutablePolicy.java | 512 ++++++++++ .../policies/ImmutablePolicyBuilder.java | 374 ++++++++ .../model/policies/PoliciesModelFactory.java | 688 ++++++++++++++ .../ditto/model/policies/PolicyBuilder.java | 894 ++++++++++++++++++ model/things/pom.xml | 138 +++ .../model/things/AttributesModelFactory.java | 114 +++ .../ditto/model/things/ImmutableFeature.java | 226 +++++ .../model/things/ThingsModelFactory.java | 853 +++++++++++++++++ .../model/things/ImmutableFeatureTest.java | 326 +++++++ .../model/things/assertions/ThingAssert.java | 602 ++++++++++++ .../protocoladapter/AbstractAdapter.java | 191 ++++ .../events/PolicyDeletedStrategy.java | 40 + .../events/PolicyEntriesModifiedStrategy.java | 38 + .../events/PolicyEntryCreatedStrategy.java | 36 + .../events/PolicyEntryDeletedStrategy.java | 36 + .../events/PolicyEntryModifiedStrategy.java | 36 + .../events/PolicyEventStrategies.java | 66 ++ .../events/PolicyModifiedStrategy.java | 40 + .../events/ResourceCreatedStrategy.java | 42 + .../events/ResourceDeletedStrategy.java | 37 + .../events/ResourceModifiedStrategy.java | 42 + .../events/ResourcesModifiedStrategy.java | 41 + .../events/SubjectCreatedStrategy.java | 42 + .../events/SubjectDeletedStrategy.java | 37 + .../events/SubjectModifiedStrategy.java | 42 + .../events/SubjectsModifiedStrategy.java | 41 + .../policies/persistence/TestConstants.java | 59 ++ .../events/AbstractThingEventStrategy.java | 71 ++ 43 files changed, 7362 insertions(+) diff --git a/json/src/main/java/org/eclipse/ditto/json/JsonPointerInvalidException.java b/json/src/main/java/org/eclipse/ditto/json/JsonPointerInvalidException.java index e69de29bb2..02f1732a22 100755 --- a/json/src/main/java/org/eclipse/ditto/json/JsonPointerInvalidException.java +++ b/json/src/main/java/org/eclipse/ditto/json/JsonPointerInvalidException.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2017 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.json; + +import java.net.URI; +import java.text.MessageFormat; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; + +/** + * Thrown if a {@link JsonPointer} was in an invalid format. + */ +public final class JsonPointerInvalidException extends JsonRuntimeException { + + /** + * Error code of this exception. + */ + public static final String ERROR_CODE = "json.pointer.invalid"; + + private static final String DEFAULT_DESCRIPTION = "Check, for example, if the JSON Pointer is not empty."; + + private static final String MULTIPLE_SLASHES_DESCRIPTION = + "Consecutive slashes in JSON pointers are not supported."; + + private static final String OUTER_SLASHES_DESCRIPTION = + "Leading or trailing slashes in JSON pointers are not supported."; + + private static final String NO_SLASHES_NO_CONTROL_CHARACTERS_DESCRIPTION = + "Neither slashes nor any control characters are allowed at any place in your JSON Pointer. Please check!"; + + private static final long serialVersionUID = -6773700329225961931L; + + private JsonPointerInvalidException(@Nullable final String message, + @Nullable final String description, + @Nullable final Throwable cause, + @Nullable final URI href) { + + super(ERROR_CODE, message, description, cause, href); + } + + /** + * Returns a builder for fluently creating instances of {@code JsonPointerInvalidException}s.. + * + * @return a new builder for JsonPointerInvalidException objects. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Returns a new builder already containing a generic message that consecutive slashes are not supported for JSON + * pointers. + * + * @param jsonPointer The JSON pointer containing the consecutive slashes. + * @return a builder for {@code JsonPointerInvalidException} objects. + */ + static JsonExceptionBuilder newBuilderForConsecutiveSlashes( + final CharSequence jsonPointer) { + return new Builder() + .jsonPointer(jsonPointer) + .description(MULTIPLE_SLASHES_DESCRIPTION); + } + + /** + * Returns a new builder already containing a generic message that leading or trailing slashes are not supported for JSON + * pointers. + * + * @param jsonPointer The JSON pointer containing the leading and/or trailing slashes. + * @return a builder for {@code JsonPointerInvalidException} objects. + */ + public static JsonExceptionBuilder newBuilderForOuterSlashes( + final CharSequence jsonPointer) { + return new Builder() + .jsonPointer(jsonPointer) + .description(OUTER_SLASHES_DESCRIPTION); + } + + /** + * Returns a new builder already containing a generic message that slashes and control characters are not supported for JSON + * pointers. + * + * @param jsonPointer The JSON pointer containing the slashes or control characters. + * @return a builder for {@code JsonPointerInvalidException} objects. + */ + public static JsonExceptionBuilder newBuilderForNoSlashesAndControlChars( + final CharSequence jsonPointer) { + return new Builder() + .jsonPointer(jsonPointer) + .description(NO_SLASHES_NO_CONTROL_CHARACTERS_DESCRIPTION); + } + + /** + * Returns a new builder containing the given message for the given JSON pointers. + * + * @param jsonPointer The JSON pointer the message is about. + * @param description The description to be in the exception. + * @return a builder for {@code JsonPointerInvalidException} objects. + */ + public static JsonExceptionBuilder newBuilderWithDescription( + final CharSequence jsonPointer, final String description) { + return new Builder() + .jsonPointer(jsonPointer) + .description(description); + } + + + /** + * Returns a new builder already containing a default message that the JSON pointer is no valid. + * + * @param jsonPointer The JSON pointer the message is about. + * @return a builder for {@code JsonPointerInvalidException} objects. + */ + public static JsonExceptionBuilder newBuilderWithoutDescription( + final CharSequence jsonPointer) { + return new Builder() + .jsonPointer(jsonPointer) + .description(DEFAULT_DESCRIPTION); + } + + /** + * A mutable builder for a {@code JsonPointerInvalidException}. + */ + @NotThreadSafe + public static final class Builder extends AbstractJsonExceptionBuilder { + + private Builder() { + super(ERROR_CODE); + description(DEFAULT_DESCRIPTION); + } + + /** + * Sets a message which points to the name of the invalid JSON pointer. Thus if this method is called, {@link + * #message(String)} should not be called. + * + * @param jsonPointerString the string representation of the invalid JSON pointer. + * @return this builder to allow method chaining. + */ + public Builder jsonPointer(@Nullable final CharSequence jsonPointerString) { + message(MessageFormat.format("The JSON pointer <{0}> is invalid!", jsonPointerString)); + return this; + } + + @Override + protected JsonPointerInvalidException doBuild(final String errorCode, + @Nullable final String message, + @Nullable final String description, + @Nullable final Throwable cause, + @Nullable final URI href) { + + return new JsonPointerInvalidException(message, description, cause, href); + } + + } + +} diff --git a/model/base/pom.xml b/model/base/pom.xml index e69de29bb2..042130f281 100755 --- a/model/base/pom.xml +++ b/model/base/pom.xml @@ -0,0 +1,88 @@ + + + + 4.0.0 + + + org.eclipse.ditto + ditto-model + ${revision} + + + ditto-model-base + bundle + Eclipse Ditto :: Model :: Base + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + org/eclipse/ditto/model/base/assertions/* + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + !org.eclipse.ditto.utils.jsr305.annotations, + org.eclipse.ditto.* + + + org.eclipse.ditto.model.base.* + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + + + + + diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityId.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityId.java index e69de29bb2..1392d13ea9 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityId.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityId.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019 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.model.base.entity.id; + +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.NAMESPACE_DELIMITER; + +import java.util.Objects; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.model.base.entity.validation.EntityIdPatternValidator; + +/** + * Default implementation for a validated {@link org.eclipse.ditto.model.base.entity.id.NamespacedEntityId} + */ +@Immutable +public final class DefaultNamespacedEntityId implements NamespacedEntityId { + + private static final NamespacedEntityId DUMMY_ID = DefaultNamespacedEntityId.of(":_"); + private static final String DEFAULT_NAMESPACE = ""; + + private final String namespace; + private final String name; + private final String stringRepresentation; + + private DefaultNamespacedEntityId(final String namespace, final String name, final boolean shouldValidate) { + if (shouldValidate) { + stringRepresentation = EntityIdPatternValidator.getInstance().validate(namespace, name); + } else { + stringRepresentation = namespace + NAMESPACE_DELIMITER + name; + } + + this.namespace = namespace; + this.name = name; + } + + private DefaultNamespacedEntityId(@Nullable final CharSequence entityId) { + if (entityId == null) { + throw NamespacedEntityIdInvalidException.newBuilder(entityId).build(); + } + + final EntityIdPatternValidator validator = EntityIdPatternValidator.getInstance(); + + if (validator.isValid(entityId)) { + namespace = validator.getNamespace(); + name = validator.getName(); + stringRepresentation = validator.getString(); + } else { + throw NamespacedEntityIdInvalidException.newBuilder(entityId).build(); + } + } + + /** + * Returns a {@link NamespacedEntityId} based on the given entityId CharSequence. May return the same instance as + * the parameter if the given parameter is already a DefaultNamespacedEntityId. Skips validation if the given + * {@code entityId} is an instance of NamespacedEntityId. + * + * @param entityId the entity ID. + * @return the namespaced entity ID. + * @throws NamespacedEntityIdInvalidException if the given {@code entityId} is invalid. + */ + public static NamespacedEntityId of(final CharSequence entityId) { + if (entityId instanceof DefaultNamespacedEntityId) { + return (NamespacedEntityId) entityId; + } + + if (entityId instanceof NamespacedEntityId) { + final String namespace = ((NamespacedEntityId) entityId).getNamespace(); + final String name = ((NamespacedEntityId) entityId).getName(); + return new DefaultNamespacedEntityId(namespace, name, false); + } + + return new DefaultNamespacedEntityId(entityId); + } + + /** + * Creates {@link NamespacedEntityId} with default namespace placeholder. + * + * @param entityName the name of the entity. + * @return the created namespaced entity ID. + * @throws NamespacedEntityIdInvalidException if the given {@code entityName} is invalid. + */ + public static NamespacedEntityId fromName(final String entityName) { + return of(DEFAULT_NAMESPACE, entityName); + } + + /** + * Creates a new {@link NamespacedEntityId} with the given namespace and name. + * + * @param namespace the namespace of the entity. + * @param name the name of the entity. + * @return the created instance of {@link NamespacedEntityId} + */ + public static NamespacedEntityId of(final String namespace, final String name) { + return new DefaultNamespacedEntityId(namespace, name, true); + } + + /** + * Returns a dummy {@link NamespacedEntityId}. This ID should not be used. It can be identified by + * checking {@link NamespacedEntityId#isDummy()}. + * + * @return the dummy ID. + */ + public static NamespacedEntityId dummy() { + return DUMMY_ID; + } + + @Override + public boolean isDummy() { + return DUMMY_ID.equals(this); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final DefaultNamespacedEntityId that = (DefaultNamespacedEntityId) o; + return Objects.equals(namespace, that.namespace) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(namespace, name); + } + + + @Override + public String toString() { + return stringRepresentation; + } + +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AbstractPatternValidator.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AbstractPatternValidator.java index e69de29bb2..2a1c76a87c 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AbstractPatternValidator.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AbstractPatternValidator.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import java.util.regex.Pattern; + +import javax.annotation.concurrent.Immutable; + +/** + * This abstract implementation of {@code PatternValidator} validates that a given {@code CharSequence} is valid. + * If no {@code Pattern} is given through an overwrite of {@code getPattern()} it throws a {@code NullPointerException}. + */ +@Immutable +public abstract class AbstractPatternValidator implements PatternValidator { + + public final static int MAX_LENGTH = 256; + private String reason; + + + @Override + public boolean isValid(final CharSequence toBeValidated) { + requireNonNull(getPattern(), "The pattern to be validated against must not be null!"); + if (toBeValidated.length() > MAX_LENGTH) { + reason = "Entity properties are not allowed to exceed length of 256."; + } + return getPattern().matcher(toBeValidated).matches(); + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public Pattern getPattern() { + return null; + } + + @Override + public Optional getReason() { + return Optional.ofNullable(reason); + } +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AttributePatternValidator.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AttributePatternValidator.java index e69de29bb2..f856657410 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AttributePatternValidator.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/AttributePatternValidator.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import java.util.regex.Pattern; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class AttributePatternValidator extends AbstractPatternValidator { + + public static AttributePatternValidator getInstance() { + return new AttributePatternValidator(); + } + + @Override + public Pattern getPattern() { + return RegexPatterns.ATTRIBUTE_PATTERN; + } + +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/EntityIdPatternValidator.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/EntityIdPatternValidator.java index e69de29bb2..3154171e31 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/EntityIdPatternValidator.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/EntityIdPatternValidator.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.ENTITY_NAME_GROUP_NAME; +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.ENTITY_NAME_PATTERN; +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.ID_PATTERN; +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.NAMESPACE_DELIMITER; +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.NAMESPACE_GROUP_NAME; +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.NAMESPACE_PATTERN; + +import java.util.regex.Matcher; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.model.base.entity.id.NamespacedEntityIdInvalidException; + +@Immutable +public final class EntityIdPatternValidator extends AbstractPatternValidator { + + private RepresentationTuple tuple; + + public static EntityIdPatternValidator getInstance() { + return new EntityIdPatternValidator(); + } + + @Override + public boolean isValid(final CharSequence toBeValidated) { + if (toBeValidated.length() > MAX_LENGTH) { + return false; + } + final Matcher matcher = ID_PATTERN.matcher(toBeValidated); + if (matcher.matches()) { + this.tuple = + new RepresentationTuple(matcher.group(NAMESPACE_GROUP_NAME), matcher.group(ENTITY_NAME_GROUP_NAME)); + return true; + } + return false; + } + + public String validate(final @Nullable String namespace, final @Nullable String name) { + final String sp = namespace + NAMESPACE_DELIMITER + name; + + if (namespace == null || name == null) { + throw NamespacedEntityIdInvalidException.newBuilder(sp).build(); + } + + if (sp.length() > MAX_LENGTH) { + throw NamespacedEntityIdInvalidException.newBuilder(sp).build(); + } + + if (!NAMESPACE_PATTERN.matcher(namespace).matches() || !ENTITY_NAME_PATTERN.matcher(name).matches()) { + throw NamespacedEntityIdInvalidException.newBuilder(sp).build(); + } + + return sp; + } + + public String getNamespace() { + return this.tuple.namespace; + } + + public String getName() { + return this.tuple.name; + } + + public String getString() { + return this.tuple.stringRepresentation; + } + + private static final class RepresentationTuple { + public final String namespace; + public final String name; + public final String stringRepresentation; + + public RepresentationTuple (final String ns, final String n) { + this.namespace = ns; + this.name = n; + this.stringRepresentation = namespace + NAMESPACE_DELIMITER + name; + } + } +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/FeaturePatternValidator.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/FeaturePatternValidator.java index e69de29bb2..a0a8e8a33d 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/FeaturePatternValidator.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/FeaturePatternValidator.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import java.util.regex.Pattern; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class FeaturePatternValidator extends AbstractPatternValidator { + + public static FeaturePatternValidator getInstance() { + return new FeaturePatternValidator(); + } + + @Override + public Pattern getPattern() { + return RegexPatterns.FEATURE_PATTERN; + } +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/PatternValidator.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/PatternValidator.java index e69de29bb2..e336e0aa60 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/PatternValidator.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/PatternValidator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import java.util.regex.Pattern; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.model.base.common.Validator; + +/** + * This interface represents a general pattern validator. + */ +@Immutable +public interface PatternValidator extends Validator { + + /** + * Indicates the validation result. + * + * @return {@code true} if the validation was successful, {@code false} if the validation failed for some reason. + */ + boolean isValid(CharSequence toBeValidated); + + /** + * Returns a pattern. + * + * @return {@code Pattern}. + */ + + @Nullable + Pattern getPattern(); + +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/RegexPatterns.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/RegexPatterns.java index e69de29bb2..0633153fe8 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/RegexPatterns.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/RegexPatterns.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import java.util.regex.Pattern; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class RegexPatterns { + + public final static String SLASH = "/"; + + public static final String NAMESPACE_DELIMITER = ":"; + + public final static String CONTROL_CHARS = "\\x00-\\x1F\\x7F-\\xFF"; + + public final static String NO_CONTROL_CHARS = "[^" + CONTROL_CHARS + "]"; + + public final static String NO_CONTROL_CHARS_NO_SLASHES = "[^" + CONTROL_CHARS + SLASH + "]"; + + /** + * Name of the namespace group in the entity ID regex. + */ + public static final String NAMESPACE_GROUP_NAME = "ns"; + + /** + * Name of the entity name group in the entity ID regex. + */ + public static final String ENTITY_NAME_GROUP_NAME = "name"; + + /** + * Defines which characters are allowed to use in a namespace. + */ + public static final String ALLOWED_NAMESPACE_CHARACTERS_REGEX = "[a-zA-Z]\\w*+"; + + /** + * Defines which characters are allowed to use in a name of an entity. + */ + public static final String ALLOWED_CHARACTERS_IN_NAME = NO_CONTROL_CHARS_NO_SLASHES; + + /** + * Adds the dot to allowed characters. Its defined as separate constant because namespaces are not allowed to start + * with dots. + */ + public static final String ALLOWED_NAMESPACE_CHARACTERS_INCLUDING_DOT = + "\\." + ALLOWED_NAMESPACE_CHARACTERS_REGEX; + + /** + * The regex pattern for namespaces which validates that the namespace conforms the java package notation. + */ + public static final String NAMESPACE_REGEX = "(?<" + NAMESPACE_GROUP_NAME + ">|(?:" + + "(?:" + ALLOWED_NAMESPACE_CHARACTERS_REGEX + ")" + + "(?:" + ALLOWED_NAMESPACE_CHARACTERS_INCLUDING_DOT + ")*+))"; + + /** + * Regex pattern that matches URL escapes. E.G. %3A for a colon (':'). + */ + public static final String URL_ESCAPES = "%\\p{XDigit}{2}"; + + /** + * Adds the $ to allowed characters. Its defined as separate constant because names are not allowed to start + * with $. + */ + public static final String ALLOWED_CHARACTERS_IN_NAME_INCLUDING_DOLLAR = ALLOWED_CHARACTERS_IN_NAME + "$"; + + /** + * First part of an entity name. + */ + public static final String URI_PATH_SEGMENT = "(?:[" + ALLOWED_CHARACTERS_IN_NAME + "]|" + URL_ESCAPES + ")"; + + /** + * Second part of an entity name: This part allows the $ symbol. + */ + public static final String URI_PATH_SEGMENT_INCLUDING_DOLLAR = + "(?:[" + ALLOWED_CHARACTERS_IN_NAME_INCLUDING_DOLLAR + "]|" + URL_ESCAPES + ")"; + /** + * The regex pattern for an Entity Name. + */ + public static final String ENTITY_NAME_REGEX = "(?<" + ENTITY_NAME_GROUP_NAME + + ">" + URI_PATH_SEGMENT + URI_PATH_SEGMENT_INCLUDING_DOLLAR + "*+)"; + + /** + * The regex pattern for an Entity ID. + * Combines "namespace" pattern (java package notation + a colon) and "name" pattern. + */ + public static final String ID_REGEX = NAMESPACE_REGEX + NAMESPACE_DELIMITER + ENTITY_NAME_REGEX; + + public final static Pattern FEATURE_PATTERN = Pattern.compile("^" + NO_CONTROL_CHARS_NO_SLASHES + "+$"); + + public final static Pattern ATTRIBUTE_PATTERN = FEATURE_PATTERN; + /** + * The compiled regex pattern for namespaces. + */ + public static final Pattern NAMESPACE_PATTERN = Pattern.compile(NAMESPACE_REGEX); + + /** + * The compiled regex pattern for entity names. + */ + public static final Pattern ENTITY_NAME_PATTERN = Pattern.compile(ENTITY_NAME_REGEX); + + /** + * The compiled regex pattern for namespaced entity IDs. + */ + public static final Pattern ID_PATTERN = Pattern.compile(ID_REGEX); + + /** + * The regex pattern a Subject has to conform to. + */ + public static final Pattern SUBJECT_REGEX = Pattern.compile("^" + NO_CONTROL_CHARS + "+$"); +} diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/SubjectPatternValidator.java b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/SubjectPatternValidator.java index e69de29bb2..48d2af64f2 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/SubjectPatternValidator.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/entity/validation/SubjectPatternValidator.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 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.model.base.entity.validation; + +import static org.eclipse.ditto.model.base.entity.validation.RegexPatterns.SUBJECT_REGEX; + +import java.util.regex.Pattern; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class SubjectPatternValidator extends AbstractPatternValidator{ + + public static SubjectPatternValidator getInstance() { + return new SubjectPatternValidator(); + } + + @Override + public Pattern getPattern() { + return SUBJECT_REGEX; + } + +} diff --git a/model/base/src/test/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityIdTest.java b/model/base/src/test/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityIdTest.java index e69de29bb2..2fc5f9b6f2 100644 --- a/model/base/src/test/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityIdTest.java +++ b/model/base/src/test/java/org/eclipse/ditto/model/base/entity/id/DefaultNamespacedEntityIdTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2019 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.model.base.entity.id; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.model.base.entity.validation.RegexPatterns; +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class DefaultNamespacedEntityIdTest { + + private static final String URL_ESCAPE_EXAMPLE = "%3A"; + private static final List ALLOWED_SPECIAL_CHARACTERS_IN_NAME = Arrays.asList( + "-", "@", "&", "=", "+", ",", ".", "!", "~", "*", "'", "$", "_", ";", URL_ESCAPE_EXAMPLE, "<", ">" + ); + + private static final String VALID_NAMESPACE = "validNamespace"; + private static final String NAMESPACE_DELIMITER = ":"; + private static final String VALID_NAME = "validName"; + private static final String VALID_ID = VALID_NAMESPACE + NAMESPACE_DELIMITER + VALID_NAME; + + @Test + public void testImmutability() { + assertInstancesOf(DefaultNamespacedEntityId.class, areImmutable()); + } + + @Test + public void testEqualsAndHashcode() { + EqualsVerifier.forClass(DefaultNamespacedEntityId.class).withIgnoredFields("stringRepresentation").verify(); + } + + @Test + public void fromNameHasEmptyNamespace() { + final NamespacedEntityId namespacedEntityId = DefaultNamespacedEntityId.fromName(VALID_NAME); + assertThat(namespacedEntityId.getNamespace()).isEmpty(); + } + + @Test + public void defaultNamespacedEntityIdFromDefaultNamespacedEntityIdIsSameInstance() { + final NamespacedEntityId namespacedEntityIdOne = DefaultNamespacedEntityId.of(VALID_NAMESPACE, VALID_NAME); + final NamespacedEntityId namespacedEntityIdTwo = DefaultNamespacedEntityId.of(namespacedEntityIdOne); + assertThat((CharSequence) namespacedEntityIdOne).isSameAs(namespacedEntityIdTwo); + } + + @Test + public void defaultNamespacedEntityIdFromNamespacedEntityIdSkipsValidation() { + /* + * NEVER DO SUCH HACKS, PLEASE! The only purpose of this validation skip is to save performance because we + * can trust our own code to not implement any NamespacedEntityId without validating namespaces and names. + */ + + final String invalidNamespace = ".invalidNamespace"; + final String invalidName = "§invalidName"; + final NamespacedEntityId invalidNamespacedEntityId = new NamespacedEntityId() { + + @Override + public boolean isDummy() { + return false; + } + + @Override + public String getName() { + return invalidName; + } + + @Override + public String getNamespace() { + return invalidNamespace; + } + }; + + final NamespacedEntityId namespacedEntityId = DefaultNamespacedEntityId.of(invalidNamespacedEntityId); + + assertThat(namespacedEntityId.getNamespace()).isEqualTo(invalidNamespace); + assertThat(namespacedEntityId.getName()).isEqualTo(invalidName); + } + + @Test + public void canHaveMaximumLengthOf256Characters() { + final int maximumAllowedCharactersForName = 255 - VALID_NAMESPACE.length(); + final StringBuilder nameWithMaximumLength = new StringBuilder(); + for (int i = 0; i < maximumAllowedCharactersForName; i++) { + nameWithMaximumLength.append("a"); + } + assertValidId(VALID_NAMESPACE, nameWithMaximumLength.toString()); + } + + @Test + public void cannotHaveMoreThan256Characters() { + final int maximumAllowedCharactersForName = 256 - VALID_NAMESPACE.length(); + final StringBuilder nameWithMaximumLength = new StringBuilder(); + for (int i = 0; i < maximumAllowedCharactersForName; i++) { + nameWithMaximumLength.append("a"); + } + + assertInValidId(VALID_NAMESPACE, nameWithMaximumLength.append("a").toString()); + } + + @Test + public void nullId() { + assertThatExceptionOfType(NamespacedEntityIdInvalidException.class) + .isThrownBy(() -> DefaultNamespacedEntityId.of(null)); + } + + @Test + public void nullName() { + assertInvalidName(null); + } + + @Test + public void nullNamespace() { + assertThatExceptionOfType(NamespacedEntityIdInvalidException.class) + .isThrownBy(() -> DefaultNamespacedEntityId.of(null, VALID_NAME)); + } + + @Test + public void testConstantsAreValid() { + assertValidNamespace(VALID_NAMESPACE); + assertValidName(VALID_NAME); + } + + @Test + public void toStringConcatenatesNamespaceAndName() { + assertThat(DefaultNamespacedEntityId.of(VALID_NAMESPACE, VALID_NAME).toString()).isEqualTo(VALID_ID); + assertThat(DefaultNamespacedEntityId.of(VALID_ID).toString()).isEqualTo(VALID_ID); + } + + @Test + public void placeholderIsPlaceholder() { + assertThat(DefaultNamespacedEntityId.dummy().isDummy()).isTrue(); + } + + @Test + public void manuallyCreatedPlaceholderIsPlaceholder() { + assertThat(DefaultNamespacedEntityId.of(":_").isDummy()).isTrue(); + assertThat(DefaultNamespacedEntityId.of("", "_").isDummy()).isTrue(); + } + + @Test + public void valid() { + assertValidId("ns", "58b4d0e9-2e97-498e-a49b-470cca589c3c:"); + } + + @Test + public void nameStartsWithColon() { + assertValidName(":name"); + } + + @Test + public void emptyNamespace() { + assertValidId("", "name"); + } + + @Test + public void onlyColons() { + assertValidId("", ":::"); + } + + @Test + public void withValidSpecialCharactersInName() { + ALLOWED_SPECIAL_CHARACTERS_IN_NAME.forEach((specialCharacter) -> { + assertValidName("x" + specialCharacter); + }); + } + + @Test + public void dollarSymbolNotAllowedAtBeginningOfName() { + assertInvalidName("$foo"); + } + + @Test + public void percentSymbolNotAllowedIfNotURLEscaping() { + assertInvalidName("fo%o"); + } + + @Test + public void paragraphSymbolNotAllowed() { + assertInvalidName("f§oo"); + } + + @Test + public void numbersAfterDotInNamespacedIsNotAllowed() { + assertInvalidNamespace("ns.x.5"); + System.out.println(RegexPatterns.ID_REGEX); + } + + @Test + public void namespacesWithLeadingDotAreInvalid() { + assertInvalidNamespace(".ns"); + } + + @Test + public void numbersInNamespacesAreAllowed() { + assertValidNamespace("ns5.foo23.bar2"); + } + + @Test + public void mutliplePrecedingDotsInNamespaceAreNotAllowed() { + assertInvalidNamespace("my....namespace"); + } + + @Test + public void notSpecialCharactersInNamespacesAreAllowed() { + assertInvalidNamespace("my$namespace"); + } + + private static void assertInvalidNamespace(@Nullable final String namespace) { + assertInValidId(namespace, VALID_NAME); + } + + private static void assertValidNamespace(@Nullable final String namespace) { + assertValidId(namespace, VALID_NAME); + } + + private static void assertInvalidName(@Nullable final String name) { + assertInValidId(VALID_NAMESPACE, name); + } + + private static void assertValidName(@Nullable final String name) { + assertValidId(VALID_NAMESPACE, name); + } + + private static void assertValidId(@Nullable final String namespace, @Nullable final String name) { + final NamespacedEntityId idBySeparated = DefaultNamespacedEntityId.of(namespace, name); + assertThat(idBySeparated.getNamespace()).isEqualTo(namespace); + assertThat(idBySeparated.getName()).isEqualTo(name); + assertThat(idBySeparated.isDummy()).isFalse(); + + final NamespacedEntityId idByCombined = + DefaultNamespacedEntityId.of(concatenateNamespaceAndName(namespace, name)); + assertThat(idByCombined.getNamespace()).isEqualTo(namespace); + assertThat(idByCombined.getName()).isEqualTo(name); + assertThat(idByCombined.isDummy()).isFalse(); + } + + private static void assertInValidId(@Nullable final String namespace, @Nullable final String name) { + + assertThatExceptionOfType(NamespacedEntityIdInvalidException.class) + .isThrownBy(() -> DefaultNamespacedEntityId.of(concatenateNamespaceAndName(namespace, name))); + + assertThatExceptionOfType(NamespacedEntityIdInvalidException.class) + .isThrownBy(() -> DefaultNamespacedEntityId.of(namespace, name)); + } + + private static String concatenateNamespaceAndName(@Nullable final String namespace, @Nullable final String name) { + final String nonNullNamespace = namespace == null ? "" : namespace; + final String nonNullName = name == null ? "" : name; + + return nonNullNamespace + NAMESPACE_DELIMITER + nonNullName; + } + +} \ No newline at end of file diff --git a/model/base/src/test/java/org/eclipse/ditto/model/base/entity/validation/RegexPatternsTest.java b/model/base/src/test/java/org/eclipse/ditto/model/base/entity/validation/RegexPatternsTest.java index e69de29bb2..6794b4d526 100644 --- a/model/base/src/test/java/org/eclipse/ditto/model/base/entity/validation/RegexPatternsTest.java +++ b/model/base/src/test/java/org/eclipse/ditto/model/base/entity/validation/RegexPatternsTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2019 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.model.base.entity.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.Test; + +/** + * Unit test for {@link RegexPatterns}. + */ +public final class RegexPatternsTest { + + /** + * Also applies to entity name (thing and policy name), attributes, features & policy label + */ + private static final List BAD_FEATURE_IDS = List.of( + // slashes + "/", "//", "/abc", "abc/", "//abc", "abc//", "abc/abc", "abc//abc", + // forbidden special characters + "§", "°", "´", + // whitespaces + "\t", "\n", + // examples + "3°C", "´a", "§1", "foo/bar" + ); + + private static final List GOOD_FEATURE_IDS = List.of( + // allowed special characters + "!\"$%&()=?`*+~'#_-:.;,|<>\\{}[]^", + // whitespaces + " ", " ", "a b c", + // examples + "a", "a!", "$a", "&a", "?a", "a.:b?c(de)-f%g\"h\"\\jk|lm#p", "\"a\"", "{\"property\":123}", "foo%3A", "$foo" + ); + + /** + * Also applies to policy resource & policy subject + */ + private static final List BAD_MESSAGE_SUBJECTS = List.of( + // forbidden special characters + "§", "°", "´", + // whitespaces + "\t", "\n", + // examples + "3°C", "´a", "§1" + ); + + private static final List GOOD_MESSAGE_SUBJECTS = List.of( + // slashes + "/", "//", "/abc", "abc/", "//abc", "abc//", "abc/abc", "abc//abc", + // allowed special characters + "!\"$%&()=?`*+~'#_-:.;,|<>\\{}[]^/", + // examples + "a", "a!", "$a", "&a", "?a", "a.:b?c(de)-f%g\"h\"\\jk|lm#p", "\"a\"", "{\"property\":123}" + ); + + private static final List BAD_NAMESPACES = Arrays.asList( + "org.eclipse.", + ".org.eclipse", + "_org.eclipse", + "org._eclipse", + "1org.eclipse", + "org.1eclipse", + "org.ec lipse", + "org.", + ".org" + ); + + private static final List GOOD_NAMESPACES = Arrays.asList( + "", + "ditto.eclipse.org", + "com.google", + "Foo.bar_122_" + ); + + @Test + public void entityNameRegex() { + GOOD_FEATURE_IDS.forEach(this::assertNameMatches); + BAD_FEATURE_IDS.forEach(this::assertNameNotMatches); + } + + private void assertNameMatches(final String name) { + assertMatches(RegexPatterns.ENTITY_NAME_PATTERN, name); + } + + private void assertNameNotMatches(final String name) { + assertNotMatches(RegexPatterns.ENTITY_NAME_PATTERN, name); + } + + @Test + public void featureRegex() { + GOOD_FEATURE_IDS.forEach(this::assertFeatureMatches); + BAD_FEATURE_IDS.forEach(this::assertFeatureNotMatches); + } + + private void assertFeatureMatches(final String id) { + assertMatches(RegexPatterns.FEATURE_PATTERN, id); + } + + private void assertFeatureNotMatches(final String id) { + assertNotMatches(RegexPatterns.FEATURE_PATTERN, id); + } + + @Test + public void namespaceRegex() { + GOOD_NAMESPACES.forEach(this::assertNamespaceMatches); + BAD_NAMESPACES.forEach(this::assertNamespaceNotMatches); + } + + private void assertNamespaceMatches(final String namespace) { + assertMatches(RegexPatterns.NAMESPACE_PATTERN, namespace); + } + + private void assertNamespaceNotMatches(final String namespace) { + assertNotMatches(RegexPatterns.NAMESPACE_PATTERN, namespace); + } + + @Test + public void subjectRegex() { + GOOD_MESSAGE_SUBJECTS.forEach(this::assertSubjectMatches); + BAD_MESSAGE_SUBJECTS.forEach(this::assertSubjectNotMatches); + } + + private void assertSubjectMatches(final String subject) { + assertMatches(RegexPatterns.SUBJECT_REGEX, subject); + } + + private void assertSubjectNotMatches(final String subject) { + assertNotMatches(RegexPatterns.SUBJECT_REGEX, subject); + } + + @Test + public void idRegex() { + GOOD_NAMESPACES.forEach(namespace -> GOOD_FEATURE_IDS.forEach(name -> { + assertIdMatches(namespace + ":" + name); + })); + BAD_NAMESPACES.forEach(namespace -> BAD_FEATURE_IDS.forEach(name -> { + assertIdNotMatches(namespace + ":" + name); + })); + BAD_FEATURE_IDS.forEach(this::assertIdNotMatches); + } + + private void assertIdMatches(final String id) { + assertMatches(RegexPatterns.ID_PATTERN, id); + } + + private void assertIdNotMatches(final String id) { + assertNotMatches(RegexPatterns.ID_PATTERN, id); + } + + private void assertMatches(final Pattern pattern, final String toMatch) { + assertThat(matches(pattern, toMatch)).isTrue(); + } + + private void assertNotMatches(final Pattern pattern, final String toMatch) { + assertThat(matches(pattern, toMatch)).isFalse(); + } + + private boolean matches(final Pattern pattern, final String toMatch) { + return pattern.matcher(toMatch).matches(); + } +} diff --git a/model/messages/src/main/java/org/eclipse/ditto/model/messages/DittoMessageSubjectValueValidator.java b/model/messages/src/main/java/org/eclipse/ditto/model/messages/DittoMessageSubjectValueValidator.java index e69de29bb2..7d78d2159a 100644 --- a/model/messages/src/main/java/org/eclipse/ditto/model/messages/DittoMessageSubjectValueValidator.java +++ b/model/messages/src/main/java/org/eclipse/ditto/model/messages/DittoMessageSubjectValueValidator.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 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.model.messages; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.model.base.entity.validation.SubjectPatternValidator; +import org.eclipse.ditto.model.base.exceptions.DittoHeaderInvalidException; +import org.eclipse.ditto.model.base.headers.AbstractHeaderValueValidator; +import org.eclipse.ditto.model.base.headers.HeaderDefinition; + +/** + * This validator checks if a CharSequence is a valid ID that matches {@link org.eclipse.ditto.model.base.entity.validation.RegexPatterns#SUBJECT_REGEX}. + * If validation fails, a {@link DittoHeaderInvalidException} is thrown. + */ +@Immutable +final class DittoMessageSubjectValueValidator extends AbstractHeaderValueValidator { + + private DittoMessageSubjectValueValidator() { + super(String.class::equals); + } + + /** + * Returns an instance of {@code DittoMessageSubjectValueValidator}. + * + * @return the instance. + */ + static DittoMessageSubjectValueValidator getInstance() { + return new DittoMessageSubjectValueValidator(); + } + + @Override + protected void validateValue(final HeaderDefinition definition, final CharSequence value) { + final SubjectPatternValidator validator = SubjectPatternValidator.getInstance(); + if (!validator.isValid(value)) { + throw DittoHeaderInvalidException.newInvalidTypeBuilder(definition, value, "message subject").build(); + } + } + +} diff --git a/model/messages/src/main/java/org/eclipse/ditto/model/messages/MessageHeaderDefinition.java b/model/messages/src/main/java/org/eclipse/ditto/model/messages/MessageHeaderDefinition.java index e69de29bb2..e64f31a01b 100755 --- a/model/messages/src/main/java/org/eclipse/ditto/model/messages/MessageHeaderDefinition.java +++ b/model/messages/src/main/java/org/eclipse/ditto/model/messages/MessageHeaderDefinition.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2017 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.model.messages; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.eclipse.ditto.model.base.headers.HeaderDefinition; +import org.eclipse.ditto.model.base.headers.HeaderValueValidators; +import org.eclipse.ditto.model.base.headers.ValueValidator; + +/** + * Enumeration of definitions of well known message headers including their key and Java type. + * Note: All header keys must be lower-case; + */ +public enum MessageHeaderDefinition implements HeaderDefinition { + + /** + * Header definition for the direction of a message. + *

+ * Key: {@code "ditto-message-direction"}, Java type: String. + *

+ */ + DIRECTION("ditto-message-direction", String.class, false, false, HeaderValueValidators.getNoOpValidator()), + + /** + * Header definitions for the subject of a message. + *

+ * Key: {@code "ditto-message-subject"}, Java type: String. + *

+ */ + SUBJECT("ditto-message-subject", String.class, false, false, DittoMessageSubjectValueValidator.getInstance()), + + /** + * Header definition for the Thing ID of a message. + *

+ * Key: {@code "ditto-message-thing-id"}, Java type: String. + *

+ */ + THING_ID("ditto-message-thing-id", String.class, false, false, HeaderValueValidators.getNoOpValidator()), + + /** + * Header definition for the Feature ID of a message, if sent to a Feature. + *

+ * Key: {@code "ditto-message-feature-id"}, Java type: String. + *

+ */ + FEATURE_ID("ditto-message-feature-id", String.class, false, false, HeaderValueValidators.getNoOpValidator()), + + /** + * Header definition for the timeout in seconds of a message. + *

+ * Key: {@code "timeout"}, Java type: {@code long}. + *

+ * @deprecated since 1.1.0: replaced by {@link org.eclipse.ditto.model.base.headers.DittoHeaderDefinition#TIMEOUT} + */ + @Deprecated + TIMEOUT("timeout", long.class, true, true, TimeoutValueValidator.getInstance()), + + /** + * Header containing the timestamp of the message as ISO 8601 string. + *

+ * Key: {@code "timestamp"}, Java type: String. + *

+ */ + TIMESTAMP("timestamp", String.class, true, true, TimestampValueValidator.getInstance()), + + /** + * Header definition for the status code of a message, e. g. if a message is a response to another message. + *

+ * Key: {@code "status"}, Java type: {@code int}. + *

+ */ + STATUS_CODE("status", int.class, true, false, HttpStatusCodeValueValidator.getInstance()); + + /** + * Map to speed up lookup of header definition by key. + */ + private static final Map VALUES_BY_KEY = Arrays.stream(values()) + .collect(Collectors.toMap(MessageHeaderDefinition::getKey, Function.identity())); + + private final String key; + private final Class type; + private final Class serializationType; + private final boolean readFromExternalHeaders; + private final boolean writeToExternalHeaders; + private final ValueValidator valueValidator; + + /** + * @param theKey the key used as key for header map. + * @param theType the Java type of the header value which is associated with this definition's key. + * @param readFromExternalHeaders whether Ditto reads this header from headers sent by externals. + * @param writeToExternalHeaders whether Ditto publishes this header to externals. + */ + private MessageHeaderDefinition(final String theKey, + final Class theType, + final boolean readFromExternalHeaders, + final boolean writeToExternalHeaders, + final ValueValidator valueValidator) { + + this(theKey, theType, theType, readFromExternalHeaders, writeToExternalHeaders, valueValidator); + } + + /** + * @param theKey the key used as key for header map. + * @param theType the Java type of the header value which is associated with this definition's key. + * @param serializationType the type to which this header value should be serialized. + * @param readFromExternalHeaders whether Ditto reads this header from headers sent by externals. + * @param writeToExternalHeaders whether Ditto publishes this header to externals. + */ + private MessageHeaderDefinition(final String theKey, + final Class theType, + final Class serializationType, + final boolean readFromExternalHeaders, + final boolean writeToExternalHeaders, + final ValueValidator valueValidator) { + + key = theKey; + type = theType; + this.serializationType = serializationType; + this.readFromExternalHeaders = readFromExternalHeaders; + this.writeToExternalHeaders = writeToExternalHeaders; + this.valueValidator = valueValidator; + } + + /** + * Finds an appropriate {@code MessageHeaderDefinition} for the specified key. + * + * @param key the key to look up. + * @return the MessageHeaderDefinition or an empty Optional. + */ + public static Optional forKey(@Nullable final CharSequence key) { + return Optional.ofNullable(VALUES_BY_KEY.get(key)); + } + + @Override + public String getKey() { + return key; + } + + @Override + public Class getJavaType() { + return type; + } + + @Override + public Class getSerializationType() { + return serializationType; + } + + @Override + public boolean shouldReadFromExternalHeaders() { + return readFromExternalHeaders; + } + + @Override + public boolean shouldWriteToExternalHeaders() { + return writeToExternalHeaders; + } + + @Override + public void validateValue(@Nullable final CharSequence value) { + valueValidator.accept(this, value); + } + + @Nonnull + @Override + public String toString() { + return getKey(); + } + +} diff --git a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PipelineFunctionParameterResolverFactoryTest.java b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PipelineFunctionParameterResolverFactoryTest.java index e69de29bb2..1e597afb6b 100644 --- a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PipelineFunctionParameterResolverFactoryTest.java +++ b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PipelineFunctionParameterResolverFactoryTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2017 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.model.placeholders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.function.Predicate; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PipelineFunctionParameterResolverFactoryTest { + + private static final String KNOWN_VALUE = "expected"; + private static final String KNOWN_PLACEHOLDER = "thing:name"; + private static final PipelineFunction DUMMY = new PipelineFunctionDelete(); + + @Mock + private ExpressionResolver expressionResolver; + + @Test + public void singleStringResolverAcceptsDoubleQuotes() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String params = "(\"" + KNOWN_VALUE + "\")"; + assertThat(parameterResolver.apply(params, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + + verifyNoInteractions(expressionResolver); + } + + @Test + public void singleStringResolverAcceptsSingleQuotes() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String params = "(\'" + KNOWN_VALUE + "\')"; + assertThat(parameterResolver.apply(params, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + + verifyNoInteractions(expressionResolver); + } + + @Test + public void singleResolverThrowsExceptionOnMultipleParameters() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String paramsDoubleQuoted = "(\"" + KNOWN_VALUE + "\", \"otherValue\")"; + final String paramsSingleQuoted = "(\'" + KNOWN_VALUE + "\', \'otherValue\')"; + final String paramsPlaceholders = "(" + KNOWN_PLACEHOLDER + ", topic:full)"; + + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> parameterResolver.apply(paramsDoubleQuoted, expressionResolver, DUMMY)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> parameterResolver.apply(paramsSingleQuoted, expressionResolver, DUMMY)); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> parameterResolver.apply(paramsPlaceholders, expressionResolver, DUMMY)); + + verifyNoInteractions(expressionResolver); + } + + @Test + public void singleResolverResolvesPlaceholder() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String params = "(" + KNOWN_PLACEHOLDER + ")"; + when(expressionResolver.resolveAsPipelineElement(anyString())) + .thenReturn(PipelineElement.resolved(KNOWN_VALUE)); + + assertThat(parameterResolver.apply(params, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + + verify(expressionResolver).resolveAsPipelineElement(KNOWN_PLACEHOLDER); + } + + @Test + public void singleResolverAllowsWhitespaceInStrings() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String value = " " + KNOWN_VALUE + " "; + final String stringSingle = "(\'" + value + "\')"; + final String stringDouble = "(\"" + value + "\")"; + + assertThat(parameterResolver.apply(stringSingle, expressionResolver, DUMMY)).contains(value); + assertThat(parameterResolver.apply(stringDouble, expressionResolver, DUMMY)).contains(value); + } + + @Test + public void singleResolverAllowsWhitespaceBetweenParentheses() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String stringSingle = "( \'" + KNOWN_VALUE + "\' )"; + final String stringDouble = "( \"" + KNOWN_VALUE + "\" )"; + final String stringPlaceholder = "( " + KNOWN_PLACEHOLDER + " )"; + + when(expressionResolver.resolveAsPipelineElement(anyString())).thenReturn( + PipelineElement.resolved(KNOWN_VALUE)); + + assertThat(parameterResolver.apply(stringSingle, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + assertThat(parameterResolver.apply(stringDouble, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + assertThat(parameterResolver.apply(stringPlaceholder, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + + verify(expressionResolver).resolveAsPipelineElement(KNOWN_PLACEHOLDER); + } + + @Test + public void singlePlaceholderResolverReturnsPlaceholderIfNotResolvable() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver = + PipelineFunctionParameterResolverFactory.forStringOrPlaceholderParameter(); + + final String stringPlaceholder = "( " + KNOWN_PLACEHOLDER + " )"; + + when(expressionResolver.resolveAsPipelineElement(anyString())).thenReturn(PipelineElement.unresolved()); + + assertThat(parameterResolver.apply(stringPlaceholder, expressionResolver, DUMMY)).isEmpty(); + + verify(expressionResolver).resolveAsPipelineElement(KNOWN_PLACEHOLDER); + } + + @Test + public void singleStringResolverThrowsExceptionIfNotResolvable() { + final PipelineFunctionParameterResolverFactory.SingleParameterResolver stringResolver = + PipelineFunctionParameterResolverFactory.forStringParameter(); + final String stringPlaceholder = "( " + KNOWN_PLACEHOLDER + " )"; + + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> stringResolver.apply(stringPlaceholder, expressionResolver, DUMMY)); + + verifyNoInteractions(expressionResolver); + } + + @Test + public void singleStringResolverDoesNotResolvePlaceholders() { + + final PipelineFunctionParameterResolverFactory.SingleParameterResolver stringResolver = + PipelineFunctionParameterResolverFactory.forStringParameter(); + final String stringSingle = "(\'" + KNOWN_VALUE + "\')"; + final String stringDouble = "(\"" + KNOWN_VALUE + "\")"; + final String stringPlaceholder = "(" + KNOWN_PLACEHOLDER + ")"; + + assertThat(stringResolver.apply(stringSingle, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + assertThat(stringResolver.apply(stringDouble, expressionResolver, DUMMY)).contains(KNOWN_VALUE); + assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class) + .isThrownBy(() -> stringResolver.apply(stringPlaceholder, expressionResolver, DUMMY)); + + verifyNoInteractions(expressionResolver); + } + + @Test + public void emptyParameterResolverResolvesEmptyParentheses() { + final Predicate noParamResolver = PipelineFunctionParameterResolverFactory.forEmptyParameters(); + + assertThat(noParamResolver.test("()")).isTrue(); + assertThat(noParamResolver.test("( )")).isTrue(); + } + + @Test + public void emptyParameterResolverFailsForNonEmptyParentheses() { + final Predicate noParamResolver = PipelineFunctionParameterResolverFactory.forEmptyParameters(); + + assertThat(noParamResolver.test("(thing:id)")).isFalse(); + assertThat(noParamResolver.test("(\"val\")")).isFalse(); + assertThat(noParamResolver.test("(\'val\')")).isFalse(); + } + +} diff --git a/model/policies/src/main/java/org/eclipse/ditto/model/policies/ImmutablePolicy.java b/model/policies/src/main/java/org/eclipse/ditto/model/policies/ImmutablePolicy.java index e69de29bb2..89e371014e 100755 --- a/model/policies/src/main/java/org/eclipse/ditto/model/policies/ImmutablePolicy.java +++ b/model/policies/src/main/java/org/eclipse/ditto/model/policies/ImmutablePolicy.java @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2017 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.model.policies; + +import static org.eclipse.ditto.model.base.common.ConditionChecker.checkNotNull; +import static org.eclipse.ditto.model.policies.PoliciesModelFactory.emptyResources; +import static org.eclipse.ditto.model.policies.PoliciesModelFactory.newPolicyEntry; +import static org.eclipse.ditto.model.policies.PoliciesModelFactory.newResources; + +import java.text.MessageFormat; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.json.JsonCollectors; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; +import org.eclipse.ditto.json.JsonParseException; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.model.base.exceptions.DittoJsonException; +import org.eclipse.ditto.model.base.json.FieldType; +import org.eclipse.ditto.model.base.json.JsonSchemaVersion; + +/** + * Immutable implementation of {@link Policy}. + */ +@Immutable +final class ImmutablePolicy implements Policy { + + @Nullable private final PolicyId policyId; + private final Map entries; + @Nullable private final String namespace; + @Nullable private final PolicyLifecycle lifecycle; + @Nullable private final PolicyRevision revision; + @Nullable private final Instant modified; + + private ImmutablePolicy(@Nullable final PolicyId policyId, + final Map theEntries, + @Nullable final PolicyLifecycle lifecycle, + @Nullable final PolicyRevision revision, + @Nullable final Instant modified) { + + this.policyId = policyId; + entries = Collections.unmodifiableMap(new LinkedHashMap<>(theEntries)); + namespace = policyId == null ? null : policyId.getNamespace(); + this.lifecycle = lifecycle; + this.revision = revision; + this.modified = modified; + } + + /** + * Returns a new Policy which is initialised with the specified entries. + * + * @param policyId the ID of the new Policy. + * @param lifecycle the lifecycle of the Policy to be created. + * @param revision the revision of the Policy to be created. + * @param modified the modified timestamp of the Policy to be created. + * @param entries the entries of the Policy to be created. + * @return a new initialised Policy. + * @throws NullPointerException if {@code entries} is {@code null}. + * @throws PolicyIdInvalidException if {@code policyId} did not comply to + * {@link org.eclipse.ditto.model.base.entity.validation.RegexPatterns#ID_REGEX}. + * @deprecated Policy ID is now typed. Use + * {@link #of(PolicyId, PolicyLifecycle, PolicyRevision, java.time.Instant, Iterable)} + * instead + */ + @Deprecated + public static Policy of(@Nullable final String policyId, + @Nullable final PolicyLifecycle lifecycle, + @Nullable final PolicyRevision revision, + @Nullable final Instant modified, + final Iterable entries) { + + return of(PolicyId.of(policyId), lifecycle, revision, modified, entries); + } + + /** + * Returns a new Policy which is initialised with the specified entries. + * + * @param policyId the ID of the new Policy. + * @param lifecycle the lifecycle of the Policy to be created. + * @param revision the revision of the Policy to be created. + * @param modified the modified timestamp of the Policy to be created. + * @param entries the entries of the Policy to be created. + * @return a new initialised Policy. + * @throws NullPointerException if {@code entries} is {@code null}. + */ + public static Policy of(@Nullable final PolicyId policyId, + @Nullable final PolicyLifecycle lifecycle, + @Nullable final PolicyRevision revision, + @Nullable final Instant modified, + final Iterable entries) { + + checkNotNull(entries, "Policy entries"); + + final Map entryMap = new LinkedHashMap<>(); + entries.forEach(policyEntry -> entryMap.put(policyEntry.getLabel(), policyEntry)); + + return new ImmutablePolicy(policyId, entryMap, lifecycle, revision, modified); + } + + /** + * Creates a new {@code Policy} object from the specified JSON object. + * + * @param jsonObject a JSON object which provides the data for the Policy to be created. + * @return a new Policy which is initialised with the extracted data from {@code jsonObject}. + * @throws NullPointerException if {@code jsonObject} is {@code null}. + * @throws PolicyEntryInvalidException if an Policy entry does not contain any known permission which evaluates to + * {@code true} or {@code false}. + * @throws PolicyIdInvalidException if the parsed policy ID did not comply to + * {@link org.eclipse.ditto.model.base.entity.validation.RegexPatterns#ID_REGEX}. + */ + public static Policy fromJson(final JsonObject jsonObject) { + final PolicyId policyId = jsonObject.getValue(JsonFields.ID).map(PolicyId::of).orElse(null); + + final PolicyLifecycle readLifecycle = jsonObject.getValue(JsonFields.LIFECYCLE) + .flatMap(PolicyLifecycle::forName) + .orElse(null); + + final PolicyRevision readRevision = jsonObject.getValue(JsonFields.REVISION) + .map(PolicyRevision::newInstance) + .orElse(null); + + final Instant readModified = jsonObject.getValue(JsonFields.MODIFIED) + .map(ImmutablePolicy::tryToParseModified) + .orElse(null); + + final JsonObject readEntries = jsonObject.getValueOrThrow(JsonFields.ENTRIES); + + final Function toPolicyEntry = jsonField -> { + final JsonValue jsonValue = jsonField.getValue(); + if (!jsonValue.isObject()) { + throw new DittoJsonException(JsonParseException.newBuilder() + .message(MessageFormat.format("<{0}> is not a JSON object!", jsonValue)) + .build()); + } + return ImmutablePolicyEntry.fromJson(jsonField.getKey(), jsonValue.asObject()); + }; + + final Collection policyEntries = readEntries.stream() + .filter(jsonField -> !Objects.equals(jsonField.getKey(), JsonSchemaVersion.getJsonKey())) + .map(toPolicyEntry) + .collect(Collectors.toSet()); + + return of(policyId, readLifecycle, readRevision, readModified, policyEntries); + } + + private static Instant tryToParseModified(final CharSequence dateTime) { + try { + return Instant.parse(dateTime); + } catch (final DateTimeParseException e) { + throw new JsonParseException("The JSON object's field '" + JsonFields.MODIFIED.getPointer() + "' " + + "is not in ISO-8601 format as expected"); + } + } + + @Override + public Optional getEntityId() { + return Optional.ofNullable(policyId); + } + + @Override + public Optional getNamespace() { + return Optional.ofNullable(namespace); + } + + @Override + public Optional getLifecycle() { + return Optional.ofNullable(lifecycle); + } + + @Override + public Optional getRevision() { + return Optional.ofNullable(revision); + } + + @Override + public Optional getModified() { + return Optional.ofNullable(modified); + } + + @Override + public boolean isDeleted() { + return PolicyLifecycle.DELETED.equals(lifecycle); + } + + @Override + public Policy setEntry(final PolicyEntry policyEntry) { + checkNotNull(policyEntry, "entry to be set to this Policy"); + + final Policy result; + + final PolicyEntry existingPolicyEntry = entries.get(policyEntry.getLabel()); + if (null != existingPolicyEntry) { + if (existingPolicyEntry.equals(policyEntry)) { + result = this; + } else { + final Map entriesCopy = copyEntries(); + entriesCopy.put(policyEntry.getLabel(), policyEntry); + result = new ImmutablePolicy(policyId, entriesCopy, lifecycle, revision, modified); + } + } else { + final Map entriesCopy = copyEntries(); + entriesCopy.put(policyEntry.getLabel(), policyEntry); + result = new ImmutablePolicy(policyId, entriesCopy, lifecycle, revision, modified); + } + + return result; + } + + @Override + public Set