Skip to content

Commit 19f7047

Browse files
committed
First official attribute data types: string, number, boolean. Validating data types of input attributes. Fixed data type conversions of returned _attributes field. Added tests.
1 parent 88b4bc0 commit 19f7047

File tree

13 files changed

+367
-118
lines changed

13 files changed

+367
-118
lines changed

src/main/java/io/zentity/model/Attribute.java

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
import java.io.IOException;
77
import java.util.*;
8+
import java.util.regex.Pattern;
89

910
public class Attribute {
1011

1112
public static final Set<String> VALID_TYPES = new HashSet<>(
12-
Arrays.asList("string", "long", "integer", "short", "byte", "double", "float", "boolean")
13+
Arrays.asList("string", "number", "boolean")
1314
);
15+
private static final Pattern REGEX_EMPTY = Pattern.compile("^\\s*$");
1416

1517
private final String name;
1618
private String type = "string";
@@ -27,6 +29,85 @@ public Attribute(String name, String json) throws ValidationException, IOExcepti
2729
this.deserialize(json);
2830
}
2931

32+
public static Boolean convertTypeBoolean(JsonNode value) {
33+
if (value.isNull())
34+
return null;
35+
return value.booleanValue();
36+
}
37+
38+
public static Number convertTypeNumber(JsonNode value) {
39+
if (value.isNull())
40+
return null;
41+
else if (value.isIntegralNumber())
42+
return value.bigIntegerValue();
43+
else if (value.isFloatingPointNumber())
44+
return value.doubleValue();
45+
else
46+
return value.numberValue();
47+
}
48+
49+
public static String convertTypeString(JsonNode value) {
50+
if (value.isNull())
51+
return null;
52+
return value.textValue();
53+
}
54+
55+
public static Object convertType(String targetType, JsonNode value) throws ValidationException {
56+
switch (targetType) {
57+
case "boolean":
58+
return convertTypeBoolean(value);
59+
case "number":
60+
return convertTypeNumber(value);
61+
case "string":
62+
return convertTypeString(value);
63+
default:
64+
throw new ValidationException("'" + targetType + " is not a recognized attribute data type.");
65+
}
66+
}
67+
68+
public static boolean isTypeBoolean(JsonNode value) {
69+
return value.isBoolean();
70+
}
71+
72+
public static boolean isTypeNumber(JsonNode value) {
73+
return value.isNumber();
74+
}
75+
76+
public static boolean isTypeString(JsonNode value) {
77+
return value.isTextual();
78+
}
79+
80+
public static void validateTypeBoolean(JsonNode value) throws ValidationException {
81+
if (!isTypeBoolean(value) && !value.isNull())
82+
throw new ValidationException("Expected 'boolean' attribute data type.");
83+
}
84+
85+
public static void validateTypeNumber(JsonNode value) throws ValidationException {
86+
if (!isTypeNumber(value) && !value.isNull())
87+
throw new ValidationException("Expected 'number' attribute data type.");
88+
}
89+
90+
public static void validateTypeString(JsonNode value) throws ValidationException {
91+
if (!isTypeString(value) && !value.isNull())
92+
throw new ValidationException("Expected 'string' attribute data type.");
93+
}
94+
95+
public static void validateType(String expectedType, JsonNode value) throws ValidationException {
96+
switch (expectedType) {
97+
case "boolean":
98+
validateTypeBoolean(value);
99+
break;
100+
case "number":
101+
validateTypeNumber(value);
102+
break;
103+
case "string":
104+
validateTypeString(value);
105+
break;
106+
default:
107+
throw new ValidationException("'" + expectedType + " is not a recognized attribute data type.");
108+
}
109+
}
110+
30111
public String name() {
31112
return this.name;
32113
}
@@ -41,15 +122,17 @@ public void type(JsonNode value) throws ValidationException {
41122
}
42123

43124
private void validateName(String value) throws ValidationException {
44-
if (value.matches("^\\s*$"))
125+
if (REGEX_EMPTY.matcher(value).matches())
45126
throw new ValidationException("'attributes' has an attribute with empty name.");
46127
}
47128

48129
private void validateType(JsonNode value) throws ValidationException {
49130
if (!value.isTextual())
50131
throw new ValidationException("'attributes." + this.name + ".type' must be a string.");
132+
if (REGEX_EMPTY.matcher(value.textValue()).matches())
133+
throw new ValidationException("'attributes." + this.name + ".type'' must not be empty.");
51134
if (!VALID_TYPES.contains(value.textValue()))
52-
throw new ValidationException("'attributes." + this.name + ".type' does not support the value '" + value.textValue() + "'.");
135+
throw new ValidationException("'attributes." + this.name + ".type' has an unrecognized data type '" + value.textValue() + "'.");
53136
}
54137

55138
private void validateObject(JsonNode object) throws ValidationException {

src/main/java/io/zentity/model/Index.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
import java.io.IOException;
77
import java.util.*;
8+
import java.util.regex.Pattern;
89

910
public class Index {
1011

1112
public static final Set<String> REQUIRED_FIELDS = new HashSet<>(
1213
Arrays.asList("fields")
1314
);
15+
private static final Pattern REGEX_EMPTY = Pattern.compile("^\\s*$");
1416

1517
private final String name;
1618
private HashMap<String, IndexField> fields;
@@ -56,7 +58,7 @@ public void fields(JsonNode value) throws ValidationException {
5658
}
5759

5860
private void validateName(String value) throws ValidationException {
59-
if (value.matches("^\\s*$"))
61+
if (REGEX_EMPTY.matcher(value).matches())
6062
throw new ValidationException("'indices' has an index with an empty name.");
6163
}
6264

src/main/java/io/zentity/model/IndexField.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55

66
import java.io.IOException;
77
import java.util.*;
8+
import java.util.regex.Pattern;
89

910
public class IndexField {
1011

1112
public static final Set<String> REQUIRED_FIELDS = new HashSet<>(
1213
Arrays.asList("attribute")
1314
);
15+
private static final Pattern REGEX_EMPTY = Pattern.compile("^\\s*$");
1416

1517
private final String index;
1618
private final String name;
1719
private String attribute;
18-
private String matcher = null;
20+
private String matcher;
1921

2022
public IndexField(String index, String name, JsonNode json) throws ValidationException {
2123
validateName(name);
@@ -54,25 +56,25 @@ public String matcher() {
5456

5557
public void matcher(JsonNode value) throws ValidationException {
5658
validateMatcher(value);
57-
this.matcher = value.isTextual() ? value.textValue() : null;
59+
this.matcher = value.textValue();
5860
}
5961

6062
private void validateName(String value) throws ValidationException {
61-
if (value.matches("^\\s*$"))
63+
if (REGEX_EMPTY.matcher(value).matches())
6264
throw new ValidationException("'indices." + this.index + "' has a field with an empty name.");
6365
}
6466

6567
private void validateAttribute(JsonNode value) throws ValidationException {
6668
if (!value.isTextual())
6769
throw new ValidationException("'indices." + this.index + "." + this.name + ".attribute' must be a string.");
68-
if (value.textValue().matches("^\\s*$"))
70+
if (REGEX_EMPTY.matcher(value.textValue()).matches())
6971
throw new ValidationException("'indices." + this.index + "." + this.name + ".attribute' must not be empty.");
7072
}
7173

7274
private void validateMatcher(JsonNode value) throws ValidationException {
73-
if (!value.isTextual() && !value.isNull())
74-
throw new ValidationException("'indices." + this.index + "." + this.name + ".matcher' must be a string or null.");
75-
if (value.textValue().matches("^\\s*$"))
75+
if (!value.isTextual())
76+
throw new ValidationException("'indices." + this.index + "." + this.name + ".matcher' must be a string.");
77+
if (REGEX_EMPTY.matcher(value.textValue()).matches())
7678
throw new ValidationException("'indices." + this.index + "." + this.name + ".matcher' must not be empty.");
7779
}
7880

src/main/java/io/zentity/model/Matcher.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.io.IOException;
88
import java.util.*;
9+
import java.util.regex.Pattern;
910

1011
public class Matcher {
1112

@@ -15,6 +16,7 @@ public class Matcher {
1516
public static final Set<String> VALID_TYPES = new HashSet<>(
1617
Arrays.asList("value")
1718
);
19+
private static final Pattern REGEX_EMPTY = Pattern.compile("^\\s*$");
1820
private static final ObjectMapper mapper = new ObjectMapper();
1921

2022
private final String name;
@@ -56,7 +58,7 @@ public void type(JsonNode value) throws ValidationException {
5658
}
5759

5860
private void validateName(String value) throws ValidationException {
59-
if (value.matches("^\\s*$"))
61+
if (REGEX_EMPTY.matcher(value).matches())
6062
throw new ValidationException("'matchers' field has a matcher with an empty name.");
6163
}
6264

@@ -70,8 +72,10 @@ private void validateClause(JsonNode value) throws ValidationException {
7072
private void validateType(JsonNode value) throws ValidationException {
7173
if (!value.isTextual())
7274
throw new ValidationException("'matchers." + this.name + ".type' must be a string.");
75+
if (REGEX_EMPTY.matcher(value.textValue()).matches())
76+
throw new ValidationException("'attributes." + this.name + ".type'' must not be empty.");
7377
if (!VALID_TYPES.contains(value.textValue()))
74-
throw new ValidationException("'matchers." + this.name + ".type' does not support the value '" + value.textValue() + "'.");
78+
throw new ValidationException("'matchers." + this.name + ".type' has an unrecognized data type '" + value.textValue() + "'.");
7579
}
7680

7781
private void validateObject(JsonNode object) throws ValidationException {

src/main/java/io/zentity/model/Resolver.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
import java.io.IOException;
77
import java.util.*;
8+
import java.util.regex.Pattern;
89

910
public class Resolver {
1011

1112
public static final Set<String> REQUIRED_FIELDS = new HashSet<>(
1213
Arrays.asList("attributes")
1314
);
15+
private static final Pattern REGEX_EMPTY = Pattern.compile("^\\s*$");
1416

1517
private final String name;
1618
private ArrayList<String> attributes = new ArrayList<>();
@@ -44,7 +46,7 @@ public void attributes(JsonNode value) throws ValidationException {
4446
}
4547

4648
private void validateName(String value) throws ValidationException {
47-
if (value.matches("^\\s*$"))
49+
if (REGEX_EMPTY.matcher(value).matches())
4850
throw new ValidationException("'resolvers' has a resolver with an empty name.");
4951
}
5052

@@ -57,8 +59,8 @@ private void validateAttributes(JsonNode value) throws ValidationException {
5759
if (!attribute.isTextual())
5860
throw new ValidationException("'resolvers." + this.name + ".attributes' must be an array of strings.");
5961
String attributeName = attribute.textValue();
60-
if (attributeName == null || attributeName.matches("^\\s*$"))
61-
throw new ValidationException("'resolvers." + this.name + ".attributes' must be an array of strings.");
62+
if (attributeName == null || REGEX_EMPTY.matcher(attributeName).matches())
63+
throw new ValidationException("'resolvers." + this.name + ".attributes' must be an array of non-empty strings.");
6264
}
6365
}
6466

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.zentity.model;
22

33
public class ValidationException extends Exception {
4-
ValidationException(String message) {
4+
public ValidationException(String message) {
55
super(message);
66
}
77
}

src/main/java/io/zentity/resolution/Job.java

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import com.fasterxml.jackson.databind.SerializationFeature;
77
import com.fasterxml.jackson.databind.node.ObjectNode;
8+
import io.zentity.model.Attribute;
89
import io.zentity.model.Matcher;
910
import io.zentity.model.Model;
11+
import io.zentity.model.ValidationException;
1012
import org.elasticsearch.action.search.SearchAction;
1113
import org.elasticsearch.action.search.SearchRequestBuilder;
1214
import org.elasticsearch.action.search.SearchResponse;
@@ -247,7 +249,7 @@ private SearchResponse search(String indexName, String query) throws IOException
247249
*
248250
* @throws IOException
249251
*/
250-
private void traverse() throws IOException {
252+
private void traverse() throws IOException, ValidationException {
251253

252254
// Prepare to collect attributes from the results of these queries as the inputs to subsequent queries.
253255
HashMap<String, HashSet<Object>> nextInputAttributes = new HashMap<>();
@@ -376,35 +378,21 @@ private void traverse() throws IOException {
376378

377379
// Gather attributes from the doc. Store them in the "_attributes" field of the doc,
378380
// and include them in the attributes for subsequent queries.
379-
TreeMap<String, Object> docAttributes = new TreeMap<>();
381+
TreeMap<String, JsonNode> docAttributes = new TreeMap<>();
380382
for (String indexFieldName : this.model.indices().get(indexName).fields().keySet()) {
381383
String attributeName = this.model.indices().get(indexName).fields().get(indexFieldName).attribute();
384+
String attributeType = this.model.attributes().get(attributeName).type();
382385
// The index field name might not refer to the _source property.
383386
// If it's not in the _source, remove the last part of the index field name from the dot notation.
384387
// Index field names can reference multi-fields, which are not returned in the _source.
388+
if (!nextInputAttributes.containsKey(attributeName))
389+
nextInputAttributes.put(attributeName, new HashSet<>());
385390
if (!doc.get("_source").has(indexFieldName))
386391
indexFieldName = indexFieldName.split("\\.")[0];
387392
if (doc.get("_source").has(indexFieldName)) {
388-
Object value;
389-
if (doc.get("_source").get(indexFieldName).isBoolean())
390-
value = doc.get("_source").get(indexFieldName).booleanValue();
391-
else if (doc.get("_source").get(indexFieldName).isDouble())
392-
value = doc.get("_source").get(indexFieldName).doubleValue();
393-
else if (doc.get("_source").get(indexFieldName).isFloat())
394-
value = doc.get("_source").get(indexFieldName).floatValue();
395-
else if (doc.get("_source").get(indexFieldName).isInt())
396-
value = doc.get("_source").get(indexFieldName).intValue();
397-
else if (doc.get("_source").get(indexFieldName).isLong())
398-
value = doc.get("_source").get(indexFieldName).longValue();
399-
else if (doc.get("_source").get(indexFieldName).isShort())
400-
value = doc.get("_source").get(indexFieldName).shortValue();
401-
else if (doc.get("_source").get(indexFieldName).isNull())
402-
value = "";
403-
else
404-
value = doc.get("_source").get(indexFieldName).asText();
405-
docAttributes.put(attributeName, value);
406-
if (!nextInputAttributes.containsKey(attributeName))
407-
nextInputAttributes.put(attributeName, new HashSet<>());
393+
JsonNode valueNode = doc.get("_source").get(indexFieldName);
394+
docAttributes.put(attributeName, valueNode);
395+
Object value = Attribute.convertType(attributeType, valueNode);
408396
nextInputAttributes.get(attributeName).add(value);
409397
}
410398
}
@@ -418,24 +406,9 @@ else if (doc.get("_source").get(indexFieldName).isNull())
418406
docObjNode.remove("_source");
419407
if (this.includeAttributes) {
420408
ObjectNode docAttributesObjNode = docObjNode.putObject("_attributes");
421-
for (String attribute : docAttributes.keySet()) {
422-
Object value = docAttributes.get(attribute);
423-
if (value.getClass() == Boolean.class)
424-
docAttributesObjNode.put(attribute, (Boolean) value);
425-
else if (value.getClass() == Double.class)
426-
docAttributesObjNode.put(attribute, (Double) value);
427-
else if (value.getClass() == Float.class)
428-
docAttributesObjNode.put(attribute, (Float) value);
429-
else if (value.getClass() == Integer.class)
430-
docAttributesObjNode.put(attribute, (Integer) value);
431-
else if (value.getClass() == Long.class)
432-
docAttributesObjNode.put(attribute, (Long) value);
433-
else if (value.getClass() == Short.class)
434-
docAttributesObjNode.put(attribute, (Short) value);
435-
else if (value.getClass() == null)
436-
docAttributesObjNode.put(attribute, "");
437-
else
438-
docAttributesObjNode.put(attribute, (String) value);
409+
for (String attributeName : docAttributes.keySet()) {
410+
JsonNode values = docAttributes.get(attributeName);
411+
docAttributesObjNode.set(attributeName, values);
439412
}
440413
}
441414

@@ -451,12 +424,12 @@ else if (value.getClass() == null)
451424
return;
452425

453426
// Update input attributes for the next queries.
454-
for (String attribute : nextInputAttributes.keySet()) {
455-
if (!this.inputAttributes.containsKey(attribute))
456-
this.inputAttributes.put(attribute, new HashSet<>());
457-
for (Object value : nextInputAttributes.get(attribute)) {
458-
if (!this.inputAttributes.get(attribute).contains(value)) {
459-
this.inputAttributes.get(attribute).add(value);
427+
for (String attributeName : nextInputAttributes.keySet()) {
428+
if (!this.inputAttributes.containsKey(attributeName))
429+
this.inputAttributes.put(attributeName, new HashSet<>());
430+
for (Object value : nextInputAttributes.get(attributeName)) {
431+
if (!this.inputAttributes.get(attributeName).contains(value)) {
432+
this.inputAttributes.get(attributeName).add(value);
460433
newHits = true;
461434
}
462435
}
@@ -477,7 +450,7 @@ else if (value.getClass() == null)
477450
* @return A JSON string to be returned as the body of the response to a client.
478451
* @throws IOException
479452
*/
480-
public String run() throws IOException {
453+
public String run() throws IOException, ValidationException {
481454
try {
482455

483456
// Reset the state of the job if reusing this Job object.

0 commit comments

Comments
 (0)