Skip to content

Commit

Permalink
Created class and tests for relation models
Browse files Browse the repository at this point in the history
  • Loading branch information
Dave Moore authored and Dave Moore committed Jun 10, 2024
1 parent 401857c commit 19702c2
Show file tree
Hide file tree
Showing 11 changed files with 987 additions and 152 deletions.
51 changes: 51 additions & 0 deletions src/main/java/io/zentity/model/Validation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.zentity.model;

import io.zentity.common.Patterns;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;

import java.io.UnsupportedEncodingException;
import java.util.Locale;
import java.util.function.BiFunction;

public class Validation {

public static final int MAX_STRICT_NAME_BYTES = 255;

/**
* Validate that a name meets the same requirements as the Elasticsearch index name requirements.
*
* @param name The name of the relation type.
* @return an optional ValidationException if the type is not in a valid format.
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.10/indices-create-index.html#indices-create-api-path-params">Elasticsearch Index Name Requirements</a>
* @see org.elasticsearch.cluster.metadata.MetadataCreateIndexService#validateIndexOrAliasName
*/
public static void validateStrictName(String name) throws ValidationException {
BiFunction<String, String, String> msg = (invalidName, description) -> "Invalid name [" + invalidName + "], " + description;
if (name == null)
throw new ValidationException(msg.apply("", "must not be empty"));
if (Patterns.EMPTY_STRING.matcher(name).matches())
throw new ValidationException(msg.apply(name, "must not be empty"));
if (!Strings.validFileName(name))
throw new ValidationException(msg.apply(name, "must not contain the following characters: " + Strings.INVALID_FILENAME_CHARS));
if (name.contains("#"))
throw new ValidationException(msg.apply(name, "must not contain '#'"));
if (name.contains(":"))
throw new ValidationException(msg.apply(name, "must not contain ':'"));
if (name.charAt(0) == '_' || name.charAt(0) == '-' || name.charAt(0) == '+')
throw new ValidationException(msg.apply(name, "must not start with '_', '-', or '+'"));
int byteCount = 0;
try {
byteCount = name.getBytes("UTF-8").length;
} catch (UnsupportedEncodingException e) {
// UTF-8 should always be supported, but rethrow this if it is not for some reason
throw new ElasticsearchException("Unable to determine length of name [" + name + "]", e);
}
if (byteCount > MAX_STRICT_NAME_BYTES)
throw new ValidationException(msg.apply(name, "name is too long, (" + byteCount + " > " + MAX_STRICT_NAME_BYTES + ")"));
if (name.equals(".") || name.equals(".."))
throw new ValidationException(msg.apply(name, "must not be '.' or '..'"));
if (!name.toLowerCase(Locale.ROOT).equals(name))
throw new ValidationException(msg.apply(name, "must be lowercase"));
}
}
6 changes: 4 additions & 2 deletions src/main/java/io/zentity/model/entity/Attribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.TreeMap;
import java.util.TreeSet;

import static io.zentity.model.Validation.validateStrictName;

public class Attribute {

public static final Set<String> VALID_TYPES = new TreeSet<>(
Expand Down Expand Up @@ -116,12 +118,12 @@ private String[] parseNameFields(String name) throws ValidationException {
}

private void validateName(String value) throws ValidationException {
Model.validateStrictName(value);
validateStrictName(value);
}

private void validateNameFields(String[] nameFields) throws ValidationException {
for (String nameField : nameFields)
Model.validateStrictName(nameField);
validateStrictName(nameField);
}

private void validateScore(JsonNode value) throws ValidationException {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/io/zentity/model/entity/Matcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import java.util.TreeSet;
import java.util.regex.Pattern;

import static io.zentity.model.Validation.validateStrictName;

public class Matcher {

public static final Set<String> REQUIRED_FIELDS = new TreeSet<>(
Expand Down Expand Up @@ -121,7 +123,7 @@ public void quality(JsonNode value) throws ValidationException {
}

private void validateName(String value) throws ValidationException {
Model.validateStrictName(value);
validateStrictName(value);
}

private void validateClause(JsonNode value) throws ValidationException {
Expand Down
46 changes: 0 additions & 46 deletions src/main/java/io/zentity/model/entity/Model.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,17 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.zentity.common.Json;
import io.zentity.common.Patterns;
import io.zentity.model.ValidationException;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiFunction;

public class Model {

Expand Down Expand Up @@ -84,46 +78,6 @@ public Map<String, Resolver> resolvers() {
return this.resolvers;
}

public static final int MAX_STRICT_NAME_BYTES = 255;

/**
* Validate the name of an entity type, attribute, resolver, or matcher.
* The name requirements are the same as the Elasticsearch index name requirements.
*
* @param name The name of the entity type, attribute, resolver, or matcher.
* @return an optional ValidationException if the type is not in a valid format.
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.10/indices-create-index.html#indices-create-api-path-params">Elasticsearch Index Name Requirements</a>
* @see org.elasticsearch.cluster.metadata.MetadataCreateIndexService#validateIndexOrAliasName
*/
public static void validateStrictName(String name) throws ValidationException {
BiFunction<String, String, String> msg = (invalidName, description) -> "Invalid name [" + invalidName + "], " + description;
if (name == null)
throw new ValidationException(msg.apply("", "must not be empty"));
if (Patterns.EMPTY_STRING.matcher(name).matches())
throw new ValidationException(msg.apply(name, "must not be empty"));
if (!Strings.validFileName(name))
throw new ValidationException(msg.apply(name, "must not contain the following characters: " + Strings.INVALID_FILENAME_CHARS));
if (name.contains("#"))
throw new ValidationException(msg.apply(name, "must not contain '#'"));
if (name.contains(":"))
throw new ValidationException(msg.apply(name, "must not contain ':'"));
if (name.charAt(0) == '_' || name.charAt(0) == '-' || name.charAt(0) == '+')
throw new ValidationException(msg.apply(name, "must not start with '_', '-', or '+'"));
int byteCount = 0;
try {
byteCount = name.getBytes("UTF-8").length;
} catch (UnsupportedEncodingException e) {
// UTF-8 should always be supported, but rethrow this if it is not for some reason
throw new ElasticsearchException("Unable to determine length of name [" + name + "]", e);
}
if (byteCount > MAX_STRICT_NAME_BYTES)
throw new ValidationException(msg.apply(name, "name is too long, (" + byteCount + " > " + MAX_STRICT_NAME_BYTES + ")"));
if (name.equals(".") || name.equals(".."))
throw new ValidationException(msg.apply(name, "must not be '.' or '..'"));
if (!name.toLowerCase(Locale.ROOT).equals(name))
throw new ValidationException(msg.apply(name, "must be lowercase"));
}

/**
* Validate the nesting of attribute names. An attribute name is invalid if another attribute name overrides it.
*
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/io/zentity/model/entity/Resolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import java.util.Set;
import java.util.TreeSet;

import static io.zentity.model.Validation.validateStrictName;

public class Resolver {

public static final Set<String> REQUIRED_FIELDS = new TreeSet<>(
Expand Down Expand Up @@ -90,7 +92,7 @@ public void weight(JsonNode value) throws ValidationException {
}

private void validateName(String value) throws ValidationException {
Model.validateStrictName(value);
validateStrictName(value);
}

private void validateAttributes(JsonNode value) throws ValidationException {
Expand Down
175 changes: 175 additions & 0 deletions src/main/java/io/zentity/model/relation/Model.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* zentity
* Copyright © 2018-2024 Dave Moore
* https://zentity.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.zentity.model.relation;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.zentity.common.Json;
import io.zentity.model.ValidationException;

import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import static io.zentity.model.Validation.validateStrictName;

public class Model {

public static final Set<String> REQUIRED_FIELDS = new TreeSet<>(
Arrays.asList("index", "a", "b")
);
public static final Set<String> VALID_RELATION_DIRECTIONS = new TreeSet<>(
Arrays.asList("a>b", "a<b", "a<>b", "")
);

private String index;
private String type;
private String direction;
private String a;
private String b;

public Model(JsonNode json) throws ValidationException, JsonProcessingException {
this.deserialize(json);
}

public Model(String json) throws ValidationException, IOException {
this.deserialize(json);
}

public String index() { return this.index; }

public String type() { return this.type; }

public String direction() { return this.direction; }

public String a() { return this.a; }

public String b() { return this.b; }

private void index(String name) throws ValidationException {
validateStrictName(name);
this.index = name;
}

private void type(String type) throws ValidationException {
validateStrictName(type);
this.type = type;
}

private void direction(String direction) throws ValidationException {
this.direction = normalizeDirection(direction);
}

private void a(String entityType) throws ValidationException {
validateStrictName(entityType);
this.a = entityType;
}

private void b(String entityType) throws ValidationException {
validateStrictName(entityType);
this.b = entityType;
}

/**
* Validate a top-level field of the relation model.
*
* @param json JSON object.
* @param field Field name.
* @throws ValidationException
*/
private void validateField(JsonNode json, String field) throws ValidationException {
JsonNode value = json.get(field);
if (REQUIRED_FIELDS.contains(field) && !value.isTextual())
throw new ValidationException("'" + field + "' must be a string.");
else if (!value.isTextual() && !value.isNull())
throw new ValidationException("'" + field + "' must be a string, null, or omitted.");
}

/**
* Validate that a given direction value is one of the allowed values.
*
* @param direction The direction value.
* @throws ValidationException
*/
public static void validateDirection(String direction) throws ValidationException {
if (!VALID_RELATION_DIRECTIONS.contains(direction))
throw new ValidationException("A relation direction must be one of: \"a>b\", \"a<b\", \"a<>b\"");
}

/**
* Normalize a given direction to the allowed characters of a direction value,
* then validate the normalized direction value.
*
* @param direction An input value for a direction.
* @return The normalized value for the direction.
* @throws ValidationException
*/
public static String normalizeDirection(String direction) throws ValidationException {
String normalized = direction.toLowerCase().replaceAll("[^ab<>]", "");
validateDirection(normalized);
return normalized;
}

public void deserialize(JsonNode json) throws ValidationException {
if (!json.isObject())
throw new ValidationException("Relation model must be an object.");

// Validate the existence of required fields.
for (String field : REQUIRED_FIELDS)
if (!json.has(field))
throw new ValidationException("Relation model is missing required field '" + field + "'.");

// Validate and hold the state of fields.
Iterator<Map.Entry<String, JsonNode>> fields = json.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String fieldName = field.getKey();
validateField(json, fieldName);
JsonNode value = field.getValue();
switch (fieldName) {
case "index":
this.index(value.asText());
break;
case "type":
if (!value.isNull() && !value.asText().equals(""))
this.type(value.asText());
break;
case "direction":
if (!value.isNull() && !value.asText().equals(""))
this.direction(value.asText());
break;
case "a":
this.a(value.asText());
break;
case "b":
this.b(value.asText());
break;
default:
throw new ValidationException("'" + fieldName + "' is not a recognized field.");
}
}
}

public void deserialize(String json) throws ValidationException, IOException {
deserialize(Json.MAPPER.readTree(json));
}

}
8 changes: 1 addition & 7 deletions src/main/java/io/zentity/model/relation/Zid.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,10 @@

import io.zentity.model.ValidationException;

import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import static io.zentity.model.relation.Model.VALID_RELATION_DIRECTIONS;

public class Zid {

public static final Set<String> VALID_RELATION_DIRECTIONS = new TreeSet<>(
Arrays.asList("a>b", "a<b", "a<>b", "")
);

/**
* Encode a relation _zid.
* Normalizes the value by ensuring that entities A and B appear in lexicographical order.
Expand Down
Loading

0 comments on commit 19702c2

Please sign in to comment.