values() {
78 | return this.values;
79 | }
80 |
81 | private void validateName(String value) throws ValidationException {
82 | if (Patterns.EMPTY_STRING.matcher(value).matches())
83 | throw new ValidationException("'attributes' has an attribute with empty name.");
84 | }
85 |
86 | /**
87 | * Validate the value of "attributes".ATTRIBUTE_NAME."type".
88 | * Must be a non-empty string containing a recognized type.
89 | *
90 | * @param value The value of "attributes".ATTRIBUTE_NAME."type".
91 | * @throws ValidationException
92 | */
93 | private void validateType(String value) throws ValidationException {
94 | if (Patterns.EMPTY_STRING.matcher(value).matches())
95 | throw new ValidationException("'attributes." + this.name + ".type'' must not be empty.");
96 | if (!io.zentity.model.Attribute.VALID_TYPES.contains(value))
97 | throw new ValidationException("'attributes." + this.name + ".type' has an unrecognized type '" + value + "'.");
98 | }
99 |
100 | /**
101 | * Parse a single input attribute value. The following examples are all valid attribute structures, although the
102 | * first example would be converted to the second example.
103 | *
104 | * {
105 | * ATTRIBUTE_NAME: [
106 | * ATTRIBUTE_VALUE,
107 | * ...
108 | * ]
109 | * }
110 | *
111 | * {
112 | * ATTRIBUTE_NAME: {
113 | * "values": [
114 | * ATTRIBUTE_VALUE,
115 | * ...
116 | * ]
117 | * }
118 | * }
119 | *
120 | * {
121 | * ATTRIBUTE_NAME: {
122 | * "values": [
123 | * ATTRIBUTE_VALUE,
124 | * ...
125 | * ],
126 | * "params": {
127 | * ATTRIBUTE_PARAM_FIELD: ATTRIBUTE_PARAM_VALUE,
128 | * ...
129 | * }
130 | * }
131 | * }
132 | *
133 | * {
134 | * ATTRIBUTE_NAME: {
135 | * "params": {
136 | * ATTRIBUTE_PARAM_FIELD: ATTRIBUTE_PARAM_VALUE,
137 | * ...
138 | * }
139 | * }
140 | * }
141 | *
142 | *
143 | * @param json Attribute object of an entity model.
144 | * @throws ValidationException
145 | */
146 | public void deserialize(JsonNode json) throws ValidationException, JsonProcessingException {
147 | if (json.isNull())
148 | return;
149 | if (!json.isObject() && !json.isArray())
150 | throw new ValidationException("'attributes." + this.name + "' must be an object or array.");
151 |
152 | Iterator valuesNode = ClassUtil.emptyIterator();
153 | Iterator> paramsNode = ClassUtil.emptyIterator();
154 |
155 | // Parse values from array
156 | if (json.isArray()) {
157 | valuesNode = json.elements();
158 |
159 | } else if (json.isObject()) {
160 |
161 | // Parse values from object
162 | if (json.has("values")) {
163 | if (!json.get("values").isArray())
164 | throw new ValidationException("'attributes." + this.name + ".values' must be an array.");
165 | valuesNode = json.get("values").elements();
166 | }
167 |
168 | // Parse params from object
169 | if (json.has("params")) {
170 | if (!json.get("params").isObject())
171 | throw new ValidationException("'attributes." + this.name + ".params' must be an object.");
172 | paramsNode = json.get("params").fields();
173 | }
174 | } else {
175 | throw new ValidationException("'attributes." + this.name + "' must be an object or array.");
176 | }
177 |
178 | // Set any values or params that were specified in the input.
179 | while (valuesNode.hasNext()) {
180 | JsonNode valueNode = valuesNode.next();
181 | this.values().add(Value.create(this.type, valueNode));
182 | }
183 |
184 | // Set any params that were specified in the input, with the values serialized as strings.
185 | while (paramsNode.hasNext()) {
186 | Map.Entry paramNode = paramsNode.next();
187 | String paramField = paramNode.getKey();
188 | JsonNode paramValue = paramNode.getValue();
189 | if (paramValue.isObject() || paramValue.isArray())
190 | this.params().put(paramField, Json.MAPPER.writeValueAsString(paramValue));
191 | else if (paramValue.isNull())
192 | this.params().put(paramField, "null");
193 | else
194 | this.params().put(paramField, paramValue.asText());
195 | }
196 | }
197 |
198 | public void deserialize(String json) throws ValidationException, IOException {
199 | deserialize(Json.MAPPER.readTree(json));
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/Term.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import io.zentity.common.Patterns;
23 | import io.zentity.model.ValidationException;
24 | import io.zentity.resolution.input.value.BooleanValue;
25 | import io.zentity.resolution.input.value.DateValue;
26 | import io.zentity.resolution.input.value.NumberValue;
27 | import io.zentity.resolution.input.value.StringValue;
28 |
29 | import java.io.IOException;
30 | import java.text.ParseException;
31 | import java.text.SimpleDateFormat;
32 |
33 | public class Term implements Comparable {
34 |
35 | private final String term;
36 | private Boolean isBoolean;
37 | private Boolean isDate;
38 | private Boolean isNumber;
39 | private BooleanValue booleanValue;
40 | private DateValue dateValue;
41 | private NumberValue numberValue;
42 | private StringValue stringValue;
43 |
44 | public Term(String term) throws ValidationException {
45 | validateTerm(term);
46 | this.term = term;
47 | }
48 |
49 | private void validateTerm(String term) throws ValidationException {
50 | if (Patterns.EMPTY_STRING.matcher(term).matches())
51 | throw new ValidationException("A term must be a non-empty string.");
52 | }
53 |
54 | public String term() { return this.term; }
55 |
56 | public static boolean isBoolean(String term) {
57 | String termLowerCase = term.toLowerCase();
58 | return termLowerCase.equals("true") || termLowerCase.equals("false");
59 | }
60 |
61 | public static boolean isDate(String term, String format) {
62 | try {
63 | SimpleDateFormat formatter = new SimpleDateFormat(format);
64 | formatter.setLenient(false);
65 | formatter.parse(term);
66 | } catch (ParseException e) {
67 | return false;
68 | }
69 | return true;
70 | }
71 |
72 | public static boolean isNumber(String term) {
73 | return Patterns.NUMBER_STRING.matcher(term).matches();
74 | }
75 |
76 | /**
77 | * Check if the term string is a boolean value.
78 | * Lazily store the decision and then return the decision.
79 | *
80 | * @return
81 | */
82 | public boolean isBoolean() {
83 | if (this.isBoolean == null)
84 | this.isBoolean = isBoolean(this.term);
85 | return this.isBoolean;
86 | }
87 |
88 | /**
89 | * Check if the term string is a date value.
90 | * Lazily store the decision and then return the decision.
91 | *
92 | * @return
93 | */
94 | public boolean isDate(String format) {
95 | if (this.isDate == null)
96 | this.isDate = isDate(this.term, format);
97 | return this.isDate;
98 | }
99 |
100 | /**
101 | * Convert the term to a BooleanValue.
102 | * Lazily store the value and then return it.
103 | *
104 | * @return
105 | */
106 | public BooleanValue booleanValue() throws IOException, ValidationException {
107 | if (this.booleanValue == null) {
108 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + this.term + "}").get("value");
109 | this.booleanValue = new BooleanValue(value);
110 | }
111 | return this.booleanValue;
112 | }
113 |
114 | /**
115 | * Check if the term string is a number value.
116 | * Lazily store the decision and then return the decision.
117 | *
118 | * @return
119 | */
120 | public boolean isNumber() {
121 | if (this.isNumber == null)
122 | this.isNumber = isNumber(this.term);
123 | return this.isNumber;
124 | }
125 |
126 | /**
127 | * Convert the term to a DateValue.
128 | * Lazily store the value and then return it.
129 | *
130 | * @return
131 | */
132 | public DateValue dateValue() throws IOException, ValidationException {
133 | if (this.dateValue == null) {
134 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + Json.quoteString(this.term) + "}").get("value");
135 | this.dateValue = new DateValue(value);
136 | }
137 | return this.dateValue;
138 | }
139 |
140 | /**
141 | * Convert the term to a NumberValue.
142 | * Lazily store the value and then return it.
143 | *
144 | * @return
145 | */
146 | public NumberValue numberValue() throws IOException, ValidationException {
147 | if (this.numberValue == null) {
148 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + this.term + "}").get("value");
149 | this.numberValue = new NumberValue(value);
150 | }
151 | return this.numberValue;
152 | }
153 |
154 | /**
155 | * Convert the term to a StringValue.
156 | * Lazily store the value and then return it.
157 | *
158 | * @return
159 | */
160 | public StringValue stringValue() throws IOException, ValidationException {
161 | if (this.stringValue == null) {
162 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + Json.quoteString(this.term) + "}").get("value");
163 | this.stringValue = new StringValue(value);
164 | }
165 | return this.stringValue;
166 | }
167 |
168 | @Override
169 | public int compareTo(Term o) {
170 | return this.term.compareTo(o.term);
171 | }
172 |
173 | @Override
174 | public String toString() {
175 | return this.term;
176 | }
177 |
178 | @Override
179 | public boolean equals(Object o) { return this.hashCode() == o.hashCode(); }
180 |
181 | @Override
182 | public int hashCode() { return this.term.hashCode(); }
183 | }
184 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/scope/Exclude.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.scope;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.Model;
22 | import io.zentity.model.ValidationException;
23 |
24 | import java.io.IOException;
25 | import java.util.Iterator;
26 | import java.util.Map;
27 |
28 | public class Exclude extends ScopeField {
29 |
30 | public Exclude() {
31 | super();
32 | }
33 |
34 | @Override
35 | public void deserialize(JsonNode json, Model model) throws ValidationException, IOException {
36 | if (!json.isNull() && !json.isObject())
37 | throw new ValidationException("The 'scope.exclude' field of the request body must be an object.");
38 |
39 | // Parse and validate the "scope.exclude" fields of the request body.
40 | Iterator> fields = json.fields();
41 | while (fields.hasNext()) {
42 | Map.Entry field = fields.next();
43 | String name = field.getKey();
44 | switch (name) {
45 | case "attributes":
46 | this.attributes = parseAttributes("exclude", model, json.get("attributes"));
47 | break;
48 | case "resolvers":
49 | this.resolvers = parseResolvers("exclude", json.get("resolvers"));
50 | break;
51 | case "indices":
52 | this.indices = parseIndices("exclude", json.get("indices"));
53 | break;
54 | default:
55 | throw new ValidationException("'scope.exclude." + name + "' is not a recognized field.");
56 | }
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/scope/Include.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.scope;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.Model;
22 | import io.zentity.model.ValidationException;
23 |
24 | import java.io.IOException;
25 | import java.util.Iterator;
26 | import java.util.Map;
27 |
28 | public class Include extends ScopeField {
29 |
30 | public Include() {
31 | super();
32 | }
33 |
34 | @Override
35 | public void deserialize(JsonNode json, Model model) throws ValidationException, IOException {
36 | if (!json.isNull() && !json.isObject())
37 | throw new ValidationException("The 'scope.include' field of the request body must be an object.");
38 |
39 | // Parse and validate the "scope.include" fields of the request body.
40 | Iterator> fields = json.fields();
41 | while (fields.hasNext()) {
42 | Map.Entry field = fields.next();
43 | String name = field.getKey();
44 | switch (name) {
45 | case "attributes":
46 | this.attributes = parseAttributes("include", model, json.get("attributes"));
47 | break;
48 | case "resolvers":
49 | this.resolvers = parseResolvers("include", json.get("resolvers"));
50 | break;
51 | case "indices":
52 | this.indices = parseIndices("include", json.get("indices"));
53 | break;
54 | default:
55 | throw new ValidationException("'scope.include." + name + "' is not a recognized field.");
56 | }
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/scope/Scope.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.scope;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import io.zentity.model.Model;
23 | import io.zentity.model.ValidationException;
24 |
25 | import java.io.IOException;
26 | import java.util.Iterator;
27 | import java.util.Map;
28 |
29 | public class Scope {
30 |
31 | private Exclude exclude = new Exclude();
32 | private Include include = new Include();
33 |
34 | public Scope() {
35 | }
36 |
37 | public Exclude exclude() {
38 | return this.exclude;
39 | }
40 |
41 | public Include include() {
42 | return this.include;
43 | }
44 |
45 | public void deserialize(JsonNode json, Model model) throws ValidationException, IOException {
46 | if (!json.isNull() && !json.isObject())
47 | throw new ValidationException("The 'scope' field of the request body must be an object.");
48 |
49 | // Parse and validate the "scope.exclude" and "scope.include" fields of the request body.
50 | Iterator> fields = json.fields();
51 | while (fields.hasNext()) {
52 | Map.Entry field = fields.next();
53 | String name = field.getKey();
54 | switch (name) {
55 | case "exclude":
56 | this.exclude.deserialize(json.get("exclude"), model);
57 | break;
58 | case "include":
59 | this.include.deserialize(json.get("include"), model);
60 | break;
61 | default:
62 | throw new ValidationException("'scope." + name + "' is not a recognized field.");
63 | }
64 | }
65 |
66 | }
67 |
68 | public void deserialize(String json, Model model) throws ValidationException, IOException {
69 | deserialize(Json.MAPPER.readTree(json), model);
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/scope/ScopeField.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.scope;
19 |
20 | import com.fasterxml.jackson.core.JsonProcessingException;
21 | import com.fasterxml.jackson.databind.JsonNode;
22 | import io.zentity.common.Json;
23 | import io.zentity.model.Model;
24 | import io.zentity.model.ValidationException;
25 | import io.zentity.resolution.input.Attribute;
26 |
27 | import java.io.IOException;
28 | import java.util.Iterator;
29 | import java.util.Map;
30 | import java.util.Set;
31 | import java.util.TreeMap;
32 | import java.util.TreeSet;
33 |
34 | public abstract class ScopeField {
35 |
36 | protected Map attributes = new TreeMap<>();
37 | protected Set indices = new TreeSet<>();
38 | protected Set resolvers = new TreeSet<>();
39 |
40 | public ScopeField() {
41 | }
42 |
43 | /**
44 | * Parse and validate the "scope.*.attributes" field of the request body or URL.
45 | *
46 | * @param scopeType "exclude" or "include".
47 | * @param model The entity model.
48 | * @param scopeAttributes The "attributes" object of "scope.exclude" or "scope.include".
49 | * @return Names and values of attributes to include in the entity model.
50 | * @throws ValidationException
51 | * @throws JsonProcessingException
52 | */
53 | public static Map parseAttributes(String scopeType, Model model, JsonNode scopeAttributes) throws ValidationException, JsonProcessingException {
54 | Map attributesObj = new TreeMap<>();
55 | if (scopeAttributes.isNull())
56 | return attributesObj;
57 | if (!scopeAttributes.isObject())
58 | throw new ValidationException("'scope." + scopeType + ".attributes' must be an object.");
59 | Iterator> attributeNodes = scopeAttributes.fields();
60 | while (attributeNodes.hasNext()) {
61 | Map.Entry attribute = attributeNodes.next();
62 | String attributeName = attribute.getKey();
63 |
64 | // Validate that the attribute exists in the entity model.
65 | if (!model.attributes().containsKey(attributeName))
66 | throw new ValidationException("'" + attributeName + "' is not defined in the entity model.");
67 |
68 | // Parse the attribute values.
69 | String attributeType = model.attributes().get(attributeName).type();
70 | JsonNode valuesNode = scopeAttributes.get(attributeName);
71 | if (!valuesNode.isNull())
72 | attributesObj.put(attributeName, new Attribute(attributeName, attributeType, valuesNode));
73 | }
74 | return attributesObj;
75 | }
76 |
77 | /**
78 | * Parse and validate the "scope.*.indices" field of the request body or URL.
79 | *
80 | * @param scopeType "include" or "exclude".
81 | * @param scopeIndices The "indices" object of "scope.exclude" or "scope.include".
82 | * @return Names of indices to include in the entity model.
83 | * @throws ValidationException
84 | */
85 | public static Set parseIndices(String scopeType, JsonNode scopeIndices) throws ValidationException {
86 | Set indices = new TreeSet<>();
87 | if (scopeIndices.isNull())
88 | return indices;
89 | if (scopeIndices.isTextual()) {
90 | if (scopeIndices.asText().equals(""))
91 | throw new ValidationException("'scope." + scopeType + ".indices' must not have non-empty strings.");
92 | String index = scopeIndices.asText();
93 | indices.add(index);
94 | } else if (scopeIndices.isArray()) {
95 | for (JsonNode indexNode : scopeIndices) {
96 | if (!indexNode.isTextual())
97 | throw new ValidationException("'scope." + scopeType + ".indices' must be a string or an array of strings.");
98 | String index = indexNode.asText();
99 | if (index == null || index.equals(""))
100 | throw new ValidationException("'scope." + scopeType + ".indices' must not have non-empty strings.");
101 | indices.add(index);
102 | }
103 | } else {
104 | throw new ValidationException("'scope." + scopeType + ".indices' must be a string or an array of strings.");
105 | }
106 | return indices;
107 | }
108 |
109 | /**
110 | * Parse and validate the "scope.*.resolvers" field of the request body or URL.
111 | *
112 | * @param scopeType "include" or "exclude".
113 | * @param scopeResolvers The "resolvers" object of "scope.exclude" or "scope.include".
114 | * @return Names of resolvers to exclude from the entity model.
115 | * @throws ValidationException
116 | */
117 | public static Set parseResolvers(String scopeType, JsonNode scopeResolvers) throws ValidationException {
118 | Set resolvers = new TreeSet<>();
119 | if (scopeResolvers.isNull())
120 | return resolvers;
121 | if (scopeResolvers.isTextual()) {
122 | if (scopeResolvers.asText().equals(""))
123 | throw new ValidationException("'scope." + scopeType + ".resolvers' must not have non-empty strings.");
124 | String resolver = scopeResolvers.asText();
125 | resolvers.add(resolver);
126 | } else if (scopeResolvers.isArray()) {
127 | for (JsonNode resolverNode : scopeResolvers) {
128 | if (!resolverNode.isTextual())
129 | throw new ValidationException("'scope." + scopeType + ".resolvers' must be a string or an array of strings.");
130 | String resolver = resolverNode.asText();
131 | if (resolver == null || resolver.equals(""))
132 | throw new ValidationException("'scope." + scopeType + ".resolvers' must not have non-empty strings.");
133 | resolvers.add(resolver);
134 | }
135 | } else {
136 | throw new ValidationException("'scope." + scopeType + ".resolvers' must be a string or an array of strings.");
137 | }
138 | return resolvers;
139 | }
140 |
141 | public Map attributes() {
142 | return this.attributes;
143 | }
144 |
145 | public void attributes(Map attributes) {
146 | this.attributes = attributes;
147 | }
148 |
149 | public Set indices() {
150 | return this.indices;
151 | }
152 |
153 | public void indices(Set indices) {
154 | this.indices = indices;
155 | }
156 |
157 | public Set resolvers() {
158 | return this.resolvers;
159 | }
160 |
161 | public void resolvers(Set resolvers) {
162 | this.resolvers = resolvers;
163 | }
164 |
165 | public abstract void deserialize(JsonNode json, Model model) throws ValidationException, IOException;
166 |
167 | public void deserialize(String json, Model model) throws ValidationException, IOException {
168 | deserialize(Json.MAPPER.readTree(json), model);
169 | }
170 |
171 | }
172 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/value/BooleanValue.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.value;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.ValidationException;
22 |
23 | public class BooleanValue extends Value {
24 |
25 | public final String type = "boolean";
26 |
27 | public BooleanValue(JsonNode value) throws ValidationException {
28 | super(value);
29 | }
30 |
31 | /**
32 | * Serialize the attribute value from a JsonNode object to a String object.
33 | *
34 | * @return
35 | */
36 | @Override
37 | public String serialize(JsonNode value) {
38 | if (value.isNull())
39 | return "null";
40 | return value.asText();
41 | }
42 |
43 | /**
44 | * Validate the value.
45 | *
46 | * @param value Attribute value.
47 | * @throws ValidationException
48 | */
49 | @Override
50 | public void validate(JsonNode value) throws ValidationException {
51 | if (!value.isBoolean() && !value.isNull())
52 | throw new ValidationException("Expected '" + this.type + "' attribute data type.");
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/value/DateValue.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.value;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.ValidationException;
22 |
23 | public class DateValue extends StringValue {
24 |
25 | public final String type = "date";
26 |
27 | public DateValue(JsonNode value) throws ValidationException {
28 | super(value);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/value/NumberValue.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.value;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.ValidationException;
22 |
23 | public class NumberValue extends Value {
24 |
25 | public final String type = "number";
26 |
27 | public NumberValue(JsonNode value) throws ValidationException {
28 | super(value);
29 | }
30 |
31 | /**
32 | * Serialize the attribute value from a JsonNode object to a String object.
33 | *
34 | * @return
35 | */
36 | @Override
37 | public String serialize(JsonNode value) {
38 | if (value.isNull())
39 | return "null";
40 | else if (value.isIntegralNumber())
41 | return value.bigIntegerValue().toString();
42 | else if (value.isFloatingPointNumber())
43 | return String.valueOf(value.doubleValue());
44 | else
45 | return value.numberValue().toString();
46 | }
47 |
48 | /**
49 | * Validate the value.
50 | *
51 | * @param value Attribute value.
52 | * @throws ValidationException
53 | */
54 | @Override
55 | public void validate(JsonNode value) throws ValidationException {
56 | if (!value.isNumber() && !value.isNull())
57 | throw new ValidationException("Expected '" + this.type + "' attribute data type.");
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/value/StringValue.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.value;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.ValidationException;
22 |
23 | public class StringValue extends Value {
24 |
25 | public final String type = "string";
26 |
27 | public StringValue(JsonNode value) throws ValidationException {
28 | super(value);
29 | }
30 |
31 | /**
32 | * Serialize the attribute value from a JsonNode object to a String object.
33 | *
34 | * @return
35 | */
36 | @Override
37 | public String serialize(JsonNode value) {
38 | if (value.isNull())
39 | return "null";
40 | return value.textValue();
41 | }
42 |
43 | /**
44 | * Validate the value.
45 | *
46 | * @param value Attribute value.
47 | * @throws ValidationException
48 | */
49 | @Override
50 | public void validate(JsonNode value) throws ValidationException {
51 | if (!value.isTextual() && !value.isNull())
52 | throw new ValidationException("Expected '" + this.type + "' attribute data type.");
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/value/Value.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.value;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.ValidationException;
22 |
23 | public abstract class Value implements ValueInterface {
24 |
25 | protected final String type = "value";
26 | protected final JsonNode value;
27 | protected final String serialized;
28 |
29 | /**
30 | * Validate and hold the object of a value.
31 | *
32 | * @param value Attribute value.
33 | */
34 | Value(JsonNode value) throws ValidationException {
35 | this.validate(value);
36 | this.value = value.isNull() ? null : value;
37 | this.serialized = this.serialize(value);
38 | }
39 |
40 | /**
41 | * Factory method to construct a Value.
42 | *
43 | * @param attributeType Attribute type.
44 | * @param value Attribute value.
45 | * @return
46 | * @throws ValidationException
47 | */
48 | public static Value create(String attributeType, JsonNode value) throws ValidationException {
49 | switch (attributeType) {
50 | case "boolean":
51 | return new BooleanValue(value);
52 | case "date":
53 | return new DateValue(value);
54 | case "number":
55 | return new NumberValue(value);
56 | case "string":
57 | return new StringValue(value);
58 | default:
59 | throw new ValidationException("'" + attributeType + " is not a recognized attribute type.");
60 | }
61 | }
62 |
63 | @Override
64 | public abstract String serialize(JsonNode value);
65 |
66 | @Override
67 | public abstract void validate(JsonNode value) throws ValidationException;
68 |
69 | @Override
70 | public Object type() {
71 | return this.type;
72 | }
73 |
74 | @Override
75 | public JsonNode value() {
76 | return this.value;
77 | }
78 |
79 | @Override
80 | public String serialized() {
81 | return this.serialized;
82 | }
83 |
84 | @Override
85 | public int compareTo(Value o) {
86 | return this.serialized.compareTo(o.serialized);
87 | }
88 |
89 | @Override
90 | public String toString() {
91 | return this.serialized;
92 | }
93 |
94 | @Override
95 | public boolean equals(Object o) { return this.hashCode() == o.hashCode(); }
96 |
97 | @Override
98 | public int hashCode() { return this.serialized.hashCode(); }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/io/zentity/resolution/input/value/ValueInterface.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input.value;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.model.ValidationException;
22 |
23 | public interface ValueInterface extends Comparable {
24 |
25 | /**
26 | * Validate the attribute value. Throw an exception on validation error. Pass on success.
27 | *
28 | * @param value Attribute value.
29 | */
30 | void validate(JsonNode value) throws ValidationException;
31 |
32 | /**
33 | * Serialize the attribute value from a JsonNode object to a String object.
34 | */
35 | String serialize(JsonNode value);
36 |
37 | /**
38 | * Return the attribute type.
39 | *
40 | * @return
41 | */
42 | Object type();
43 |
44 | /**
45 | * Return the attribute value.
46 | *
47 | * @return
48 | */
49 | Object value();
50 |
51 | /**
52 | * Return the serialized attribute value.
53 | *
54 | * @return
55 | */
56 | String serialized();
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/org/elasticsearch/plugin/zentity/BulkAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import io.zentity.common.Json;
21 | import io.zentity.common.Patterns;
22 | import io.zentity.common.StreamUtil;
23 | import joptsimple.internal.Strings;
24 | import org.elasticsearch.core.Tuple;
25 |
26 | import java.util.Arrays;
27 | import java.util.List;
28 | import java.util.stream.Collectors;
29 |
30 | public class BulkAction {
31 |
32 | public static final int MAX_CONCURRENT_OPERATIONS_PER_REQUEST = 100;
33 |
34 | /**
35 | * Split an NDJSON-formatted string into pairs of params and payloads.
36 | *
37 | * @param body NDJSON-formatted string.
38 | * @return
39 | */
40 | static List> splitBulkEntries(String body) {
41 | String[] lines = Patterns.NEWLINE.split(body);
42 | if (lines.length % 2 != 0)
43 | throw new BadRequestException("Bulk request must have repeating pairs of params and payloads on separate lines.");
44 | return Arrays.stream(lines)
45 | .flatMap(StreamUtil.tupleFlatmapper())
46 | .collect(Collectors.toList());
47 | }
48 |
49 | /**
50 | * Serialize the response of a bulk request.
51 | *
52 | * @param result The result of a bulk request.
53 | * @return
54 | */
55 | static String bulkResultToJson(BulkResult result) {
56 | return "{" +
57 | Json.quoteString("took") + ":" + result.took +
58 | "," + Json.quoteString("errors") + ":" + result.errors +
59 | "," + Json.quoteString("items") + ":" + "[" + Strings.join(result.items, ",") + "]" +
60 | "}";
61 | }
62 |
63 | /**
64 | * Small wrapper around a single response for a bulk request.
65 | */
66 | static final class SingleResult {
67 | final String response;
68 | final boolean failed;
69 |
70 | SingleResult(String response, boolean failed) {
71 | this.response = response;
72 | this.failed = failed;
73 | }
74 | }
75 |
76 | /**
77 | * A wrapper for a collection of single responses for a bulk request.
78 | */
79 | static final class BulkResult {
80 | final List items;
81 | final boolean errors;
82 | final long took;
83 |
84 | BulkResult(List items, boolean errors, long took) {
85 | this.items = items;
86 | this.errors = errors;
87 | this.took = took;
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/org/elasticsearch/plugin/zentity/HomeAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import org.elasticsearch.client.internal.node.NodeClient;
21 | import org.elasticsearch.xcontent.XContentBuilder;
22 | import org.elasticsearch.xcontent.XContentFactory;
23 | import org.elasticsearch.rest.BaseRestHandler;
24 | import org.elasticsearch.rest.RestResponse;
25 | import org.elasticsearch.rest.RestRequest;
26 | import org.elasticsearch.rest.RestStatus;
27 |
28 | import java.util.List;
29 | import java.util.Properties;
30 |
31 | import static org.elasticsearch.rest.RestRequest.Method.GET;
32 |
33 |
34 | public class HomeAction extends BaseRestHandler {
35 |
36 | @Override
37 | public List routes() {
38 | return List.of(
39 | new Route(GET, "_zentity")
40 | );
41 | }
42 |
43 | @Override
44 | public String getName() {
45 | return "zentity_plugin_action";
46 | }
47 |
48 | @Override
49 | protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
50 |
51 | Properties props = ZentityPlugin.properties();
52 | Boolean pretty = restRequest.paramAsBoolean("pretty", false);
53 | return channel -> {
54 | XContentBuilder content = XContentFactory.jsonBuilder();
55 | if (pretty)
56 | content.prettyPrint();
57 | content.startObject();
58 | content.field("name", props.getProperty("name"));
59 | content.field("description", props.getProperty("description"));
60 | content.field("website", props.getProperty("zentity.website"));
61 | content.startObject("version");
62 | content.field("zentity", props.getProperty("zentity.version"));
63 | content.field("elasticsearch", props.getProperty("elasticsearch.version"));
64 | content.endObject();
65 | content.endObject();
66 | channel.sendResponse(new RestResponse(RestStatus.OK, content));
67 | };
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/org/elasticsearch/plugin/zentity/ParamsUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import org.elasticsearch.core.Booleans;
21 | import org.elasticsearch.core.TimeValue;
22 | import org.elasticsearch.rest.RestRequest;
23 |
24 | import java.util.Map;
25 | import java.util.Optional;
26 | import java.util.TreeMap;
27 | import java.util.function.Function;
28 |
29 | public class ParamsUtil {
30 | /**
31 | * Parse a string as a boolean, where empty values are interpreted as "true". Similar to
32 | * {@link RestRequest#paramAsBoolean}.
33 | *
34 | * @param val The raw param value.
35 | * @return The parsed bool.
36 | */
37 | private static boolean asBoolean(String val) {
38 | // Treat empty string as true because that allows the presence of the url parameter to mean "turn this on"
39 | if (val != null && val.length() == 0) {
40 | return true;
41 | } else {
42 | return Booleans.parseBoolean(val);
43 | }
44 | }
45 |
46 | /**
47 | * Get a parameter, which may or may not be present, from multiple sets of parameters.
48 | *
49 | * @param key The parameter key.
50 | * @param params The primary set of parameters.
51 | * @param defaultParams A backup set of parameters, if the parameter is not found in the primary.
52 | * @return An optional string of the parameter's value.
53 | */
54 | private static Optional opt(String key, Map params, Map defaultParams) {
55 | return Optional
56 | .ofNullable(params.get(key))
57 | .or(() -> Optional.ofNullable(defaultParams.get(key)));
58 | }
59 |
60 | private static T opt(String key, T defaultValue, Function mapper, Map params, Map defaultParams) {
61 | return opt(key, params, defaultParams)
62 | .map((val) -> {
63 | try {
64 | return mapper.apply(val);
65 | } catch (Exception ex) {
66 | throw new BadRequestException("Failed to parse parameter [" + key + "] with value [" + val + "]", ex);
67 | }
68 | })
69 | .orElse(defaultValue);
70 | }
71 |
72 | public static String optString(String key, String defaultValue, Map params, Map defaultParams) {
73 | return opt(key, params, defaultParams).orElse(defaultValue);
74 | }
75 |
76 | public static Boolean optBoolean(String key, Boolean defaultValue, Map params, Map defaultParams) {
77 | return opt(key, defaultValue, ParamsUtil::asBoolean, params, defaultParams);
78 | }
79 |
80 | public static Integer optInteger(String key, Integer defaultValue, Map params, Map defaultParams) {
81 | return opt(key, defaultValue, Integer::parseInt, params, defaultParams);
82 | }
83 |
84 | public static TimeValue optTimeValue(String key, TimeValue defaultValue, Map params, Map defaultParams) {
85 | return opt(key, defaultValue, s -> TimeValue.parseTimeValue(s, key), params, defaultParams);
86 | }
87 |
88 | /**
89 | * Read many parameters from a {@link RestRequest} into a {@link Map}. It is necessary to read all possible params
90 | * in a {@link org.elasticsearch.rest.BaseRestHandler BaseRestHandler's} prepare method to avoid throwing
91 | * a validation error.
92 | *
93 | * @param req A request.
94 | * @param params All the parameters to read from the request.
95 | * @return A map from the parameter name to the parameter value in the request.
96 | */
97 | public static Map readAll(RestRequest req, String ...params) {
98 | Map paramsMap = new TreeMap<>();
99 |
100 | for (String param : params) {
101 | paramsMap.put(param, req.param(param));
102 | }
103 |
104 | return paramsMap;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/org/elasticsearch/plugin/zentity/SetupAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import org.apache.logging.log4j.LogManager;
21 | import org.apache.logging.log4j.Logger;
22 | import org.elasticsearch.ElasticsearchSecurityException;
23 | import org.elasticsearch.action.ActionListener;
24 | import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
25 | import org.elasticsearch.client.internal.node.NodeClient;
26 | import org.elasticsearch.common.settings.Settings;
27 | import org.elasticsearch.xcontent.XContentBuilder;
28 | import org.elasticsearch.xcontent.XContentFactory;
29 | import org.elasticsearch.rest.BaseRestHandler;
30 | import org.elasticsearch.rest.RestRequest;
31 | import org.elasticsearch.rest.RestResponse;
32 | import org.elasticsearch.rest.RestStatus;
33 |
34 | import java.util.List;
35 |
36 | import static org.elasticsearch.rest.RestRequest.Method;
37 | import static org.elasticsearch.rest.RestRequest.Method.POST;
38 |
39 | public class SetupAction extends BaseRestHandler {
40 |
41 | private static final Logger logger = LogManager.getLogger(SetupAction.class);
42 |
43 | public static final int DEFAULT_NUMBER_OF_SHARDS = 1;
44 | public static final int DEFAULT_NUMBER_OF_REPLICAS = 1;
45 | public static final String INDEX_MAPPING = "{\n" +
46 | " \"dynamic\": \"strict\",\n" +
47 | " \"properties\": {\n" +
48 | " \"attributes\": {\n" +
49 | " \"type\": \"object\",\n" +
50 | " \"enabled\": false\n" +
51 | " },\n" +
52 | " \"resolvers\": {\n" +
53 | " \"type\": \"object\",\n" +
54 | " \"enabled\": false\n" +
55 | " },\n" +
56 | " \"matchers\": {\n" +
57 | " \"type\": \"object\",\n" +
58 | " \"enabled\": false\n" +
59 | " },\n" +
60 | " \"indices\": {\n" +
61 | " \"type\": \"object\",\n" +
62 | " \"enabled\": false\n" +
63 | " }\n" +
64 | " }\n" +
65 | "}";
66 |
67 | @Override
68 | public List routes() {
69 | return List.of(
70 | new Route(POST, "_zentity/_setup")
71 | );
72 | }
73 |
74 | /**
75 | * Create the .zentity-models index.
76 | *
77 | * @param client The client that will communicate with Elasticsearch.
78 | * @param numberOfShards The value of index.number_of_shards.
79 | * @param numberOfReplicas The value of index.number_of_replicas.
80 | * @param onComplete Action to perform after index creation request completes.
81 | */
82 | public static void createIndex(NodeClient client, int numberOfShards, int numberOfReplicas, ActionListener onComplete) {
83 | client.admin().indices().prepareCreate(ModelsAction.INDEX_NAME)
84 | .setSettings(Settings.builder()
85 | .put("index.hidden", true)
86 | .put("index.number_of_shards", numberOfShards)
87 | .put("index.number_of_replicas", numberOfReplicas)
88 | )
89 | .setMapping(INDEX_MAPPING)
90 | .execute(onComplete);
91 | }
92 |
93 | /**
94 | * Create the .zentity-models index using the default settings.
95 | *
96 | * @param client The client that will communicate with Elasticsearch.
97 | * @param onComplete The action to perform after the index creation request completes.
98 | */
99 | public static void createIndex(NodeClient client, ActionListener onComplete) {
100 | createIndex(client, DEFAULT_NUMBER_OF_SHARDS, DEFAULT_NUMBER_OF_REPLICAS, onComplete);
101 | }
102 |
103 | @Override
104 | public String getName() {
105 | return "zentity_setup_action";
106 | }
107 |
108 | @Override
109 | protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
110 |
111 | // Parse request
112 | Boolean pretty = restRequest.paramAsBoolean("pretty", false);
113 | int numberOfShards = restRequest.paramAsInt("number_of_shards", DEFAULT_NUMBER_OF_SHARDS);
114 | int numberOfReplicas = restRequest.paramAsInt("number_of_replicas", DEFAULT_NUMBER_OF_REPLICAS);
115 | Method method = restRequest.method();
116 |
117 | return channel -> {
118 | try {
119 | if (method == POST) {
120 | createIndex(client, numberOfShards, numberOfReplicas, new ActionListener<>() {
121 |
122 | @Override
123 | public void onResponse(CreateIndexResponse response) {
124 | try {
125 |
126 | // The .zentity-models index was created. Send the response.
127 | XContentBuilder content = XContentFactory.jsonBuilder();
128 | if (pretty)
129 | content.prettyPrint();
130 | content.startObject().field("acknowledged", true).endObject();
131 | channel.sendResponse(new RestResponse(RestStatus.OK, content));
132 | } catch (Exception e) {
133 |
134 | // An error occurred when sending the response.
135 | ZentityPlugin.sendResponseError(channel, logger, e);
136 | }
137 | }
138 |
139 | @Override
140 | public void onFailure(Exception e) {
141 |
142 | // An error occurred when creating the .zentity-models index.
143 | if (e.getClass() == ElasticsearchSecurityException.class) {
144 |
145 | // The error was a security exception.
146 | // Log the error message as it was received from Elasticsearch.
147 | logger.debug(e.getMessage());
148 |
149 | // Return a more descriptive error message for the user.
150 | ZentityPlugin.sendResponseError(channel, logger, new ForbiddenException("The '" + ModelsAction.INDEX_NAME + "' index cannot be created. This action requires the 'create_index' privilege for the '" + ModelsAction.INDEX_NAME + "' index. Your role does not have this privilege."));
151 | } else {
152 |
153 | // The error was unexpected.
154 | ZentityPlugin.sendResponseError(channel, logger, e);
155 | }
156 | }
157 | });
158 |
159 | } else {
160 | throw new NotImplementedException("Method and endpoint not implemented.");
161 | }
162 | } catch (NotImplementedException e) {
163 | channel.sendResponse(new RestResponse(channel, RestStatus.NOT_IMPLEMENTED, e));
164 | }
165 | };
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/java/org/elasticsearch/plugin/zentity/ZentityPlugin.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import io.zentity.common.Json;
21 | import io.zentity.model.ValidationException;
22 | import org.apache.logging.log4j.Logger;
23 | import org.elasticsearch.ElasticsearchException;
24 | import org.elasticsearch.ElasticsearchSecurityException;
25 | import org.elasticsearch.ElasticsearchStatusException;
26 | import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
27 | import org.elasticsearch.cluster.node.DiscoveryNodes;
28 | import org.elasticsearch.common.Strings;
29 | import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
30 | import org.elasticsearch.common.settings.ClusterSettings;
31 | import org.elasticsearch.common.settings.IndexScopedSettings;
32 | import org.elasticsearch.common.settings.Settings;
33 | import org.elasticsearch.common.settings.SettingsFilter;
34 | import org.elasticsearch.features.NodeFeature;
35 | import org.elasticsearch.rest.RestChannel;
36 | import org.elasticsearch.rest.RestController;
37 | import org.elasticsearch.rest.RestHandler;
38 | import org.elasticsearch.rest.RestResponse;
39 | import org.elasticsearch.rest.RestStatus;
40 | import org.elasticsearch.xcontent.XContentBuilder;
41 | import org.elasticsearch.plugins.ActionPlugin;
42 | import org.elasticsearch.plugins.Plugin;
43 |
44 | import java.io.IOException;
45 | import java.io.InputStream;
46 | import java.util.Arrays;
47 | import java.util.List;
48 | import java.util.Properties;
49 | import java.util.function.Predicate;
50 | import java.util.function.Supplier;
51 |
52 | class NotFoundException extends Exception {
53 | public NotFoundException(String message) {
54 | super(message);
55 | }
56 | }
57 |
58 | class NotImplementedException extends Exception {
59 | NotImplementedException(String message) {
60 | super(message);
61 | }
62 | }
63 |
64 | class ForbiddenException extends ElasticsearchSecurityException {
65 | public ForbiddenException(String message) {
66 | super(message);
67 | }
68 | }
69 |
70 | class BadRequestException extends ElasticsearchStatusException {
71 | public BadRequestException(String message) {
72 | this(message, null);
73 | }
74 |
75 | public BadRequestException(String message, Throwable cause) {
76 | super(message, RestStatus.BAD_REQUEST, cause);
77 | }
78 | }
79 |
80 | public class ZentityPlugin extends Plugin implements ActionPlugin {
81 |
82 | private static final Properties properties = new Properties();
83 |
84 | public ZentityPlugin() throws IOException {
85 | Properties zentityProperties = new Properties();
86 | Properties pluginDescriptorProperties = new Properties();
87 | InputStream zentityStream = this.getClass().getResourceAsStream("/zentity.properties");
88 | InputStream pluginDescriptorStream = this.getClass().getResourceAsStream("/plugin-descriptor.properties");
89 | zentityProperties.load(zentityStream);
90 | pluginDescriptorProperties.load(pluginDescriptorStream);
91 | properties.putAll(zentityProperties);
92 | properties.putAll(pluginDescriptorProperties);
93 | }
94 |
95 | public static Properties properties() {
96 | return properties;
97 | }
98 |
99 | public String version() {
100 | return properties.getProperty("version");
101 | }
102 |
103 | @Override
104 | public List getRestHandlers(
105 | Settings settings,
106 | NamedWriteableRegistry namedWriteableRegistry,
107 | RestController restController,
108 | ClusterSettings clusterSettings,
109 | IndexScopedSettings indexScopedSettings,
110 | SettingsFilter settingsFilter,
111 | IndexNameExpressionResolver indexNameExpressionResolver,
112 | Supplier nodesInCluster,
113 | Predicate clusterSupportsFeature) {
114 | return Arrays.asList(
115 | new HomeAction(),
116 | new ModelsAction(),
117 | new ResolutionAction(),
118 | new SetupAction()
119 | );
120 | }
121 |
122 | /**
123 | * Return an error response through a RestChannel.
124 | * This method is used by the action classes in org.elasticsearch.plugin.zentity.
125 | *
126 | * @param channel The REST channel to return the response through.
127 | * @param e The exception object to process and return.
128 | */
129 | protected static void sendResponseError(RestChannel channel, Logger logger, Exception e) {
130 | try {
131 |
132 | // Handle known types of errors.
133 | if (e instanceof ForbiddenException) {
134 | channel.sendResponse(new RestResponse(channel, RestStatus.FORBIDDEN, e));
135 | } else if (e instanceof ValidationException) {
136 | channel.sendResponse(new RestResponse(channel, RestStatus.BAD_REQUEST, e));
137 | } else if (e instanceof NotFoundException) {
138 | channel.sendResponse(new RestResponse(channel, RestStatus.NOT_FOUND, e));
139 | } else if (e instanceof NotImplementedException) {
140 | channel.sendResponse(new RestResponse(channel, RestStatus.NOT_IMPLEMENTED, e));
141 | } else if (e instanceof ElasticsearchException) {
142 | // Any other ElasticsearchException which has its own status code.
143 | channel.sendResponse(new RestResponse(channel, ((ElasticsearchException) e).status(), e));
144 | } else {
145 | // Log the stack trace for unexpected types of errors.
146 | logger.catching(e);
147 | channel.sendResponse(new RestResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, e));
148 | }
149 | } catch (Exception ee) {
150 |
151 | // Unexpected error when processing the exception object.
152 | // Since BytesRestResponse throws IOException, build this response object as a string
153 | // to handle any IOException that find its way here.
154 | logger.catching(ee);
155 | String message = "{\"error\":{\"root_cause\":[{\"type\":\"exception\",\"reason\":" + Json.quoteString(ee.getMessage()) + "}],\"type\":\"exception\",\"reason\":" + Json.quoteString(ee.getMessage()) + "},\"status\":500}";
156 | channel.sendResponse(new RestResponse(RestStatus.INTERNAL_SERVER_ERROR, message));
157 | }
158 | }
159 |
160 | /**
161 | * Return a response through a RestChannel.
162 | * This method is used by the action classes in org.elasticsearch.plugin.zentity.
163 | *
164 | * @param channel The REST channel to return the response through.
165 | * @param content The content to process and return.
166 | */
167 | protected static void sendResponse(RestChannel channel, RestStatus statusCode, XContentBuilder content) {
168 | channel.sendResponse(new RestResponse(statusCode, content));
169 | }
170 |
171 | /**
172 | * Return a response through a RestChannel.
173 | * This method is used by the action classes in org.elasticsearch.plugin.zentity.
174 | *
175 | * @param channel The REST channel to return the response through.
176 | * @param content The content to process and return.
177 | */
178 | protected static void sendResponse(RestChannel channel, XContentBuilder content) {
179 | sendResponse(channel, RestStatus.OK, Strings.toString(content));
180 | }
181 |
182 | /**
183 | * Return a response through a RestChannel.
184 | * This method is used by the action classes in org.elasticsearch.plugin.zentity.
185 | *
186 | * @param channel The REST channel to return the response through.
187 | * @param json The JSON string to process and return.
188 | */
189 | protected static void sendResponse(RestChannel channel, RestStatus statusCode, String json) {
190 | channel.sendResponse(new RestResponse(statusCode, "application/json", json));
191 | }
192 |
193 | /**
194 | * Return a response through a RestChannel.
195 | * This method is used by the action classes in org.elasticsearch.plugin.zentity.
196 | *
197 | * @param channel The REST channel to return the response through.
198 | * @param json The JSON string to process and return.
199 | */
200 | protected static void sendResponse(RestChannel channel, String json) {
201 | sendResponse(channel, RestStatus.OK, json);
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/main/resources/license-header-notice.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ^\s*$
5 | ^\s*$
6 | true
7 | true
8 | false
9 |
10 |
--------------------------------------------------------------------------------
/src/main/resources/license-header.txt:
--------------------------------------------------------------------------------
1 | ${project.name}
2 | Copyright © ${project.inceptionYear}-${current.year} ${owner}
3 | ${project.url}
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
--------------------------------------------------------------------------------
/src/main/resources/notice-template.ftl:
--------------------------------------------------------------------------------
1 | <#-- To render the third-party file.
2 | Available context :
3 | - dependencyMap a collection of Map.Entry with
4 | key are dependencies (as a MavenProject) (from the maven project)
5 | values are licenses of each dependency (array of string)
6 | - licenseMap a collection of Map.Entry with
7 | key are licenses of each dependency (array of string)
8 | values are all dependencies using this license
9 | -->
10 |
11 | <#function projectLicense licenses>
12 | <#if dependencyMap?size == 0>
13 | <#list licenses as license>
14 | <#return license>
15 | #list>
16 | <#else>
17 | <#assign result = ""/>
18 | <#list licenses as license>
19 | <#if result == "">
20 | <#assign result = license/>
21 | <#else>
22 | <#assign result = result + ", " + license/>
23 | #if>
24 | #list>
25 | <#return result>
26 | #if>
27 | #function>
28 |
29 | <#function projectName p>
30 | <#if p.name?index_of('Unnamed') > -1>
31 | <#return p.artifactId>
32 | <#else>
33 | <#return p.name>
34 | #if>
35 | #function>
36 |
37 | <#if dependencyMap?size == 0>
38 | The project has no dependencies.
39 | <#else>
40 |
41 | ================================================================================
42 |
43 | zentity uses the following third-party dependencies:
44 |
45 | <#list dependencyMap as e>
46 | <#assign project = e.getKey()/>
47 | <#assign licenses = e.getValue()/>
48 | --------------------------------------------------------------------------------
49 |
50 | Name: ${projectName(project)}
51 | Artifact: ${project.groupId}:${project.artifactId}:${project.version}
52 | URL: ${project.url!"-"}
53 | License: ${projectLicense(licenses)}
54 |
55 | #list>
56 | #if>
57 | --------------------------------------------------------------------------------
58 |
--------------------------------------------------------------------------------
/src/main/resources/plugin-descriptor.properties:
--------------------------------------------------------------------------------
1 | classname=${zentity.classname}
2 | description=${project.description}
3 | elasticsearch.version=${elasticsearch.version}
4 | java.version=${jdk.version}
5 | name=${project.artifactId}
6 | version=${project.version}
7 |
--------------------------------------------------------------------------------
/src/main/resources/zentity.properties:
--------------------------------------------------------------------------------
1 | zentity.author=${zentity.author}
2 | zentity.website=${zentity.website}
3 | zentity.version=${zentity.version}
--------------------------------------------------------------------------------
/src/test/java/io/zentity/common/AsyncCollectionRunnerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.common;
19 |
20 | import org.elasticsearch.action.ActionListener;
21 | import org.junit.Test;
22 |
23 | import java.util.Collection;
24 | import java.util.List;
25 | import java.util.concurrent.CompletableFuture;
26 | import java.util.concurrent.ExecutionException;
27 | import java.util.concurrent.Executor;
28 | import java.util.function.BiConsumer;
29 | import java.util.stream.Collectors;
30 | import java.util.stream.IntStream;
31 |
32 | import static org.junit.Assert.assertEquals;
33 |
34 | public class AsyncCollectionRunnerTest {
35 | static final Executor THREAD_PER_TASK_EXECUTOR = (command) -> new Thread(command).start();
36 |
37 | void quietSleep(long millis) {
38 | try {
39 | Thread.sleep(millis);
40 | } catch (InterruptedException ignored) {
41 | }
42 | }
43 |
44 | @Test
45 | public void testRunSerial() throws InterruptedException, ExecutionException {
46 | List items = List.of(0, 1, 2, 3, 4);
47 |
48 | BiConsumer> itemRunner = (num, listener) -> THREAD_PER_TASK_EXECUTOR.execute(() -> {
49 | quietSleep(1);
50 | listener.onResponse(num);
51 | });
52 |
53 | CompletableFuture> doneFut = new CompletableFuture<>();
54 |
55 | AsyncCollectionRunner runner = new AsyncCollectionRunner<>(
56 | items,
57 | itemRunner);
58 |
59 | runner.run(ActionListener.wrap(doneFut::complete, doneFut::completeExceptionally));
60 |
61 | Collection results = doneFut.get();
62 | assertEquals(items, results);
63 | }
64 |
65 | @Test
66 | public void testRunParallel() throws InterruptedException, ExecutionException {
67 | int size = 1_000;
68 | List items = IntStream.range(0, size)
69 | .boxed()
70 | .collect(Collectors.toList());
71 |
72 | BiConsumer> itemRunner = (num, listener) -> THREAD_PER_TASK_EXECUTOR.execute(() -> {
73 | quietSleep(1);
74 | listener.onResponse(num);
75 | });
76 |
77 | CompletableFuture> doneFut = new CompletableFuture<>();
78 |
79 | AsyncCollectionRunner runner = new AsyncCollectionRunner<>(
80 | items,
81 | itemRunner,
82 | 50);
83 |
84 | runner.run(
85 | ActionListener.wrap(doneFut::complete, doneFut::completeExceptionally));
86 |
87 | Collection results = doneFut.get();
88 | assertEquals(items, results);
89 | }
90 |
91 | @Test
92 | public void testRunHigherConcurrencyThanItems() throws InterruptedException, ExecutionException {
93 | int size = 4;
94 | List items = IntStream.range(0, size)
95 | .boxed()
96 | .collect(Collectors.toList());
97 |
98 | BiConsumer> itemRunner = (num, listener) -> THREAD_PER_TASK_EXECUTOR.execute(() -> {
99 | quietSleep(1);
100 | listener.onResponse(num);
101 | });
102 |
103 | CompletableFuture> doneFut = new CompletableFuture<>();
104 |
105 | AsyncCollectionRunner runner = new AsyncCollectionRunner<>(
106 | items,
107 | itemRunner,
108 | 50);
109 |
110 | runner.run(
111 | ActionListener.wrap(doneFut::complete, doneFut::completeExceptionally));
112 |
113 | Collection results = doneFut.get();
114 | assertEquals(items, results);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/test/java/io/zentity/common/StreamUtilTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.common;
19 |
20 | import org.elasticsearch.core.Tuple;
21 | import org.junit.Test;
22 |
23 | import java.util.List;
24 | import java.util.stream.Collectors;
25 | import java.util.stream.Stream;
26 |
27 | import static org.junit.Assert.assertEquals;
28 |
29 | public class StreamUtilTest {
30 | @Test
31 | public void testTupleFlatmapper() {
32 | Stream stream = Stream.of("0a", "0b", "1a", "1b", "2a", "2b");
33 |
34 | List> tuples = stream
35 | .flatMap(StreamUtil.tupleFlatmapper())
36 | .collect(Collectors.toList());
37 |
38 | assertEquals(3, tuples.size());
39 |
40 | for (int i = 0; i < tuples.size(); i++) {
41 | Tuple tup = tuples.get(i);
42 | assertEquals(i + "a", tup.v1());
43 | assertEquals(i + "b", tup.v2());
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/java/io/zentity/model/IndexFieldTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.model;
19 |
20 | import org.junit.Test;
21 |
22 | public class IndexFieldTest {
23 |
24 | public final static String VALID_OBJECT = "{\"attribute\":\"foo\",\"matcher\":\"bar\",\"quality\":1.0}";
25 |
26 | //// "indices".INDEX_NAME."fields" ///////////////////////////////////////////////////////////////////////////////
27 |
28 | @Test
29 | public void testValid() throws Exception {
30 | new IndexField("index_name", "index_field_name", VALID_OBJECT);
31 | }
32 |
33 | @Test(expected = ValidationException.class)
34 | public void testInvalidUnexpectedField() throws Exception {
35 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":\"bar\",\"quality\":1.0,\"foo\":\"bar\"}");
36 | }
37 |
38 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME //////////////////////////////////////////////////////////////
39 |
40 | @Test(expected = ValidationException.class)
41 | public void testInvalidNameEmpty() throws Exception {
42 | new IndexField("index_name", " ", VALID_OBJECT);
43 | }
44 |
45 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME."attribute" //////////////////////////////////////////////////
46 |
47 | @Test(expected = ValidationException.class)
48 | public void testInvalidAttributeMissing() throws Exception {
49 | new IndexField("index_name", "index_field_name", "{\"matcher\":\"bar\"}");
50 | }
51 |
52 | @Test(expected = ValidationException.class)
53 | public void testInvalidAttributeEmpty() throws Exception {
54 | new IndexField("index_name", "index_field_name", "{\"attribute\":\" \",\"matcher\":\"bar\"}");
55 | }
56 |
57 | @Test(expected = ValidationException.class)
58 | public void testInvalidAttributeTypeArray() throws Exception {
59 | new IndexField("index_name", "index_field_name", "{\"attribute\":[],\"matcher\":\"bar\"}");
60 | }
61 |
62 | @Test(expected = ValidationException.class)
63 | public void testInvalidAttributeTypeBoolean() throws Exception {
64 | new IndexField("index_name", "index_field_name", "{\"attribute\":true,\"matcher\":\"bar\"}");
65 | }
66 |
67 | @Test(expected = ValidationException.class)
68 | public void testInvalidAttributeTypeFloat() throws Exception {
69 | new IndexField("index_name", "index_field_name", "{\"attribute\":1.0,\"matcher\":\"bar\"}");
70 | }
71 |
72 | @Test(expected = ValidationException.class)
73 | public void testInvalidAttributeTypeInteger() throws Exception {
74 | new IndexField("index_name", "index_field_name", "{\"attribute\":1,\"matcher\":\"bar\"}");
75 | }
76 |
77 | @Test(expected = ValidationException.class)
78 | public void testInvalidAttributeTypeNull() throws Exception {
79 | new IndexField("index_name", "index_field_name", "{\"attribute\":null,\"matcher\":\"bar\"}");
80 | }
81 |
82 | @Test(expected = ValidationException.class)
83 | public void testInvalidAttributeTypeObject() throws Exception {
84 | new IndexField("index_name", "index_field_name", "{\"attribute\":{},\"matcher\":\"bar\"}");
85 | }
86 |
87 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME."matcher" ////////////////////////////////////////////////////
88 |
89 | /**
90 | * Valid because matchers are optional for index fields.
91 | * See: https://zentity.io/docs/advanced-usage/payload-attributes/
92 | */
93 | @Test
94 | public void testValidMatcherMissing() throws Exception {
95 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\"}");
96 | }
97 |
98 | /**
99 | * Valid because matchers are optional for index fields.
100 | * See: https://zentity.io/docs/advanced-usage/payload-attributes/
101 | */
102 | @Test
103 | public void testValidMatcherTypeNull() throws Exception {
104 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":null}");
105 | }
106 |
107 | @Test(expected = ValidationException.class)
108 | public void testInvalidMatcherEmpty() throws Exception {
109 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":\" \"}");
110 | }
111 |
112 | @Test(expected = ValidationException.class)
113 | public void testInvalidMatcherTypeArray() throws Exception {
114 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":[]}");
115 | }
116 |
117 | @Test(expected = ValidationException.class)
118 | public void testInvalidMatcherTypeBoolean() throws Exception {
119 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":true}");
120 | }
121 |
122 | @Test(expected = ValidationException.class)
123 | public void testInvalidMatcherTypeFloat() throws Exception {
124 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":1.0}");
125 | }
126 |
127 | @Test(expected = ValidationException.class)
128 | public void testInvalidMatcherTypeInteger() throws Exception {
129 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":1}");
130 | }
131 |
132 | @Test(expected = ValidationException.class)
133 | public void testInvalidMatcherTypeObject() throws Exception {
134 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":{}}");
135 | }
136 |
137 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME."quality" ////////////////////////////////////////////////////
138 |
139 | @Test
140 | public void testValidQualityValue() throws Exception {
141 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0.0}");
142 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0.5}");
143 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":1.0}");
144 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0}");
145 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":1}");
146 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":null}");
147 | }
148 |
149 | /**
150 | * Valid because the "quality" field is optional.
151 | */
152 | @Test
153 | public void testValidQualityMissing() throws Exception {
154 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\"}");
155 | }
156 |
157 | /**
158 | * Valid because the "quality" field is optional.
159 | */
160 | @Test
161 | public void testValidQualityTypeNull() throws Exception {
162 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":null}");
163 | }
164 |
165 | @Test
166 | public void testValidQualityTypeIntegerOne() throws Exception {
167 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":1}");
168 | }
169 |
170 | @Test
171 | public void testValidQualityTypeIntegerZero() throws Exception {
172 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0}");
173 | }
174 |
175 | @Test(expected = ValidationException.class)
176 | public void testInvalidQualityTypeArray() throws Exception {
177 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":[]}");
178 | }
179 |
180 | @Test(expected = ValidationException.class)
181 | public void testInvalidQualityTypeBoolean() throws Exception {
182 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":true}");
183 | }
184 |
185 | @Test(expected = ValidationException.class)
186 | public void testInvalidQualityTypeInteger() throws Exception {
187 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":10}");
188 | }
189 |
190 | @Test(expected = ValidationException.class)
191 | public void testInvalidQualityTypeFloatNegative() throws Exception {
192 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":-1.0}");
193 | }
194 |
195 | @Test(expected = ValidationException.class)
196 | public void testInvalidQualityTypeObject() throws Exception {
197 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":{}}");
198 | }
199 |
200 | @Test(expected = ValidationException.class)
201 | public void testInvalidQualityValueTooHigh() throws Exception {
202 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":100.0}");
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/src/test/java/io/zentity/model/IndexTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.model;
19 |
20 | import org.junit.Test;
21 |
22 | public class IndexTest {
23 |
24 | public final static String VALID_OBJECT = "{\"fields\":{\"index_field_name\":" + IndexFieldTest.VALID_OBJECT + "}}";
25 |
26 | //// "indices" ///////////////////////////////////////////////////////////////////////////////////////////////////
27 |
28 | @Test
29 | public void testValid() throws Exception {
30 | new Index("index_name", VALID_OBJECT);
31 | }
32 |
33 | @Test(expected = ValidationException.class)
34 | public void testInvalidUnexpectedField() throws Exception {
35 | new Index("index_name", "{\"fields\":{\"index_field_name\":" + IndexFieldTest.VALID_OBJECT + "},\"foo\":\"bar\"}");
36 | }
37 |
38 | //// "indices".INDEX_NAME ////////////////////////////////////////////////////////////////////////////////////////
39 |
40 | @Test(expected = ValidationException.class)
41 | public void testInvalidNameEmpty() throws Exception {
42 | new Index(" ", VALID_OBJECT);
43 | }
44 |
45 | //// "indices".INDEX_NAME."fields" ///////////////////////////////////////////////////////////////////////////////
46 |
47 | @Test(expected = ValidationException.class)
48 | public void testInvalidFieldsMissing() throws Exception {
49 | new Index("index_name", "{}");
50 | }
51 |
52 | @Test
53 | public void testValidFieldsEmpty() throws Exception {
54 | new Index("index_name", "{\"fields\":{}}");
55 | }
56 |
57 | @Test(expected = ValidationException.class)
58 | public void testInvalidFieldsEmptyRunnable() throws Exception {
59 | new Index("index_name", "{\"fields\":{}}", true);
60 | }
61 |
62 | @Test(expected = ValidationException.class)
63 | public void testInvalidFieldsTypeArray() throws Exception {
64 | new Index("index_name", "{\"fields\":[]}");
65 | }
66 |
67 | @Test(expected = ValidationException.class)
68 | public void testInvalidFieldsTypeBoolean() throws Exception {
69 | new Index("index_name", "{\"fields\":true}");
70 | }
71 |
72 | @Test(expected = ValidationException.class)
73 | public void testInvalidFieldsTypeFloat() throws Exception {
74 | new Index("index_name", "{\"fields\":1.0}");
75 | }
76 |
77 | @Test(expected = ValidationException.class)
78 | public void testInvalidFieldsTypeInteger() throws Exception {
79 | new Index("index_name", "{\"fields\":1}");
80 | }
81 |
82 | @Test(expected = ValidationException.class)
83 | public void testInvalidFieldsTypeNull() throws Exception {
84 | new Index("index_name", "{\"fields\":null}");
85 | }
86 |
87 | @Test(expected = ValidationException.class)
88 | public void testInvalidFieldsTypeString() throws Exception {
89 | new Index("index_name", "{\"fields\":\"foobar\"}");
90 | }
91 | }
--------------------------------------------------------------------------------
/src/test/java/io/zentity/model/MatcherTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.model;
19 |
20 | import org.junit.Test;
21 |
22 | import java.util.Collections;
23 |
24 | public class MatcherTest {
25 |
26 | public final static String VALID_OBJECT = "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}}}";
27 |
28 | //// "matchers" //////////////////////////////////////////////////////////////////////////////////////////////////
29 |
30 | @Test
31 | public void testValid() throws Exception {
32 | new Matcher("matcher_name", VALID_OBJECT);
33 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0.5}");
34 | }
35 |
36 | @Test(expected = ValidationException.class)
37 | public void testInvalidUnexpectedField() throws Exception {
38 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"foo\":\"bar\"}");
39 | }
40 |
41 | //// "matchers".MATCHER_NAME /////////////////////////////////////////////////////////////////////////////////////
42 |
43 | @Test(expected = ValidationException.class)
44 | public void testInvalidNameEmpty() throws Exception {
45 | new Matcher(" ", VALID_OBJECT);
46 | }
47 |
48 | @Test(expected = ValidationException.class)
49 | public void testInvalidNameContainsAsterisk() throws Exception {
50 | new Matcher("selectivemploymentax*", VALID_OBJECT);
51 | }
52 |
53 | @Test(expected = ValidationException.class)
54 | public void testInvalidNameContainsHash() throws Exception {
55 | new Matcher("c#ke", VALID_OBJECT);
56 | }
57 |
58 | @Test(expected = ValidationException.class)
59 | public void testInvalidNameContainsColon() throws Exception {
60 | new Matcher("p:psi", VALID_OBJECT);
61 | }
62 |
63 | @Test(expected = ValidationException.class)
64 | public void testInvalidNameStartsWithUnderscore() throws Exception {
65 | new Matcher("_fanta", VALID_OBJECT);
66 | }
67 |
68 | @Test(expected = ValidationException.class)
69 | public void testInvalidNameStartsWithDash() throws Exception {
70 | new Matcher("-fanta", VALID_OBJECT);
71 | }
72 |
73 | @Test(expected = ValidationException.class)
74 | public void testInvalidNameStartsWithPlus() throws Exception {
75 | new Matcher("+fanta", VALID_OBJECT);
76 | }
77 |
78 | @Test(expected = ValidationException.class)
79 | public void testInvalidNameStartsTooLong() throws Exception {
80 | new Matcher(String.join("", Collections.nCopies(100, "sprite")), VALID_OBJECT);
81 | }
82 |
83 | @Test(expected = ValidationException.class)
84 | public void testInvalidNameIsDot() throws Exception {
85 | new Matcher(".", VALID_OBJECT);
86 | }
87 |
88 | @Test(expected = ValidationException.class)
89 | public void testInvalidNameIsDotDot() throws Exception {
90 | new Matcher("..", VALID_OBJECT);
91 | }
92 |
93 | @Test(expected = ValidationException.class)
94 | public void testInvalidNameIsNotLowercase() throws Exception {
95 | new Matcher("MELLO_yello", VALID_OBJECT);
96 | }
97 |
98 | @Test
99 | public void testValidNames() throws Exception {
100 | new Matcher("hello", VALID_OBJECT);
101 | new Matcher(".hello", VALID_OBJECT);
102 | new Matcher("..hello", VALID_OBJECT);
103 | new Matcher("hello.world", VALID_OBJECT);
104 | new Matcher("hello_world", VALID_OBJECT);
105 | new Matcher("hello-world", VALID_OBJECT);
106 | new Matcher("hello+world", VALID_OBJECT);
107 | new Matcher("您好", VALID_OBJECT);
108 | }
109 |
110 | //// "matchers".MATCHER_NAME."clause" ////////////////////////////////////////////////////////////////////////////
111 |
112 | @Test(expected = ValidationException.class)
113 | public void testInvalidClauseEmpty() throws Exception {
114 | new Matcher("matcher_name", "{\"clause\":{}}");
115 | }
116 |
117 | @Test(expected = ValidationException.class)
118 | public void testInvalidClauseTypeArray() throws Exception {
119 | new Matcher("matcher_name", "{\"clause\":[]}");
120 | }
121 |
122 | @Test(expected = ValidationException.class)
123 | public void testInvalidClauseTypeBoolean() throws Exception {
124 | new Matcher("matcher_name", "{\"clause\":true}");
125 | }
126 |
127 | @Test(expected = ValidationException.class)
128 | public void testInvalidClauseTypeFloat() throws Exception {
129 | new Matcher("matcher_name", "{\"clause\":1.0}");
130 | }
131 |
132 | @Test(expected = ValidationException.class)
133 | public void testInvalidClauseTypeInteger() throws Exception {
134 | new Matcher("matcher_name", "{\"clause\":1}");
135 | }
136 |
137 | @Test(expected = ValidationException.class)
138 | public void testInvalidClauseTypeNull() throws Exception {
139 | new Matcher("matcher_name", "{\"clause\":null}");
140 | }
141 |
142 | @Test(expected = ValidationException.class)
143 | public void testInvalidClauseTypeString() throws Exception {
144 | new Matcher("matcher_name", "{\"clause\":\"foobar\"}");
145 | }
146 |
147 | //// "matchers".MATCHER_NAME."quality" ///////////////////////////////////////////////////////////////////////////
148 |
149 | @Test
150 | public void testValidQualityValue() throws Exception {
151 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0.0}");
152 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0.5}");
153 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":1.0}");
154 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0}");
155 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":1}");
156 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":null}");
157 | }
158 |
159 | /**
160 | * Valid because the "quality" field is optional.
161 | */
162 | @Test
163 | public void testValidQualityMissing() throws Exception {
164 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}}}");
165 | }
166 |
167 | /**
168 | * Valid because the "quality" field is optional.
169 | */
170 | @Test
171 | public void testValidQualityTypeNull() throws Exception {
172 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":null}");
173 | }
174 |
175 | @Test
176 | public void testValidQualityTypeIntegerOne() throws Exception {
177 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":1}");
178 | }
179 |
180 | @Test
181 | public void testValidQualityTypeIntegerZero() throws Exception {
182 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0}");
183 | }
184 |
185 | @Test(expected = ValidationException.class)
186 | public void testInvalidQualityTypeArray() throws Exception {
187 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":[]}");
188 | }
189 |
190 | @Test(expected = ValidationException.class)
191 | public void testInvalidQualityTypeBoolean() throws Exception {
192 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":true}");
193 | }
194 |
195 | @Test(expected = ValidationException.class)
196 | public void testInvalidQualityTypeInteger() throws Exception {
197 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":10}");
198 | }
199 |
200 | @Test(expected = ValidationException.class)
201 | public void testInvalidQualityTypeFloatNegative() throws Exception {
202 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":-1.0}");
203 | }
204 |
205 | @Test(expected = ValidationException.class)
206 | public void testInvalidQualityTypeObject() throws Exception {
207 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":{}}");
208 | }
209 |
210 | @Test(expected = ValidationException.class)
211 | public void testInvalidQualityValueTooHigh() throws Exception {
212 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":100.0}");
213 | }
214 |
215 | }
216 |
--------------------------------------------------------------------------------
/src/test/java/io/zentity/resolution/input/TermTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package io.zentity.resolution.input;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import io.zentity.model.ValidationException;
23 | import io.zentity.resolution.input.value.Value;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | public class TermTest {
28 |
29 | //// "terms".TERM ////////////////////////////////////////////////////////////////////////////////////////////////
30 |
31 | @Test(expected = ValidationException.class)
32 | public void testInvalidTypeEmptyString() throws Exception {
33 | new Term("");
34 | }
35 | @Test(expected = ValidationException.class)
36 | public void testInvalidTypeEmptyStringWhitespace() throws Exception {
37 | new Term(" ");
38 | }
39 |
40 | //// Value type detection ////////////////////////////////////////////////////////////////////////////////////////
41 |
42 | @Test
43 | public void testValidTypeBooleanFalse() throws Exception {
44 | Term term = new Term("false");
45 | Assert.assertTrue(term.isBoolean());
46 | Assert.assertFalse(term.isNumber());
47 | }
48 |
49 | @Test
50 | public void testValidTypeBooleanTrue() throws Exception {
51 | Term term = new Term("true");
52 | Assert.assertTrue(term.isBoolean());
53 | Assert.assertFalse(term.isNumber());
54 | }
55 |
56 | @Test
57 | public void testValidTypeDate() throws Exception {
58 | Term term = new Term("2019-12-31 12:45:00");
59 | Assert.assertFalse(term.isBoolean());
60 | Assert.assertTrue(term.isDate("yyyy-MM-dd HH:mm:ss"));
61 | Assert.assertFalse(term.isNumber());
62 | }
63 |
64 | @Test
65 | public void testInvalidTypeDate() throws Exception {
66 | Term term = new Term("2019-12-31 12:45:00");
67 | Assert.assertFalse(term.isDate("yyyyMMdd"));
68 | }
69 |
70 | @Test
71 | public void testValidTypeNumberIntegerLongNegative() throws Exception {
72 | Term term = new Term("-922337203685477");
73 | Assert.assertFalse(term.isBoolean());
74 | Assert.assertTrue(term.isNumber());
75 | }
76 |
77 | @Test
78 | public void testValidTypeNumberIntegerLongPositive() throws Exception {
79 | Term term = new Term("922337203685477");
80 | Assert.assertFalse(term.isBoolean());
81 | Assert.assertTrue(term.isNumber());
82 | }
83 |
84 | @Test
85 | public void testValidTypeNumberIntegerShortNegative() throws Exception {
86 | Term term = new Term("-1");
87 | Assert.assertFalse(term.isBoolean());
88 | Assert.assertTrue(term.isNumber());
89 | }
90 |
91 | @Test
92 | public void testValidTypeNumberIntegerShortPositive() throws Exception {
93 | Term term = new Term("1");
94 | Assert.assertFalse(term.isBoolean());
95 | Assert.assertTrue(term.isNumber());
96 | }
97 |
98 | @Test
99 | public void testValidTypeNumberFloatLongNegative() throws Exception {
100 | Term term = new Term("-3.141592653589793");
101 | Assert.assertFalse(term.isBoolean());
102 | Assert.assertTrue(term.isNumber());
103 | }
104 |
105 | @Test
106 | public void testValidTypeNumberFloatLongPositive() throws Exception {
107 | Term term = new Term("3.141592653589793");
108 | Assert.assertFalse(term.isBoolean());
109 | Assert.assertTrue(term.isNumber());
110 | }
111 |
112 | @Test
113 | public void testValidTypeNumberFloatShortNegative() throws Exception {
114 | Term term = new Term("-1.0");
115 | Assert.assertFalse(term.isBoolean());
116 | Assert.assertTrue(term.isNumber());
117 | }
118 |
119 | @Test
120 | public void testValidTypeNumberFloatShortPositive() throws Exception {
121 | Term term = new Term("1.0");
122 | Assert.assertFalse(term.isBoolean());
123 | Assert.assertTrue(term.isNumber());
124 | }
125 |
126 | //// Value conversion ////////////////////////////////////////////////////////////////////////////////////////////
127 |
128 | @Test
129 | public void testValueConversionBooleanFalse() throws Exception {
130 | Term term = new Term("false");
131 | JsonNode value = Json.MAPPER.readTree("{\"value\":false}").get("value");
132 | Assert.assertEquals(term.booleanValue(), Value.create("boolean", value));
133 | }
134 |
135 | @Test
136 | public void testValueConversionBooleanTrue() throws Exception {
137 | Term term = new Term("true");
138 | JsonNode value = Json.MAPPER.readTree("{\"value\":true}").get("value");
139 | Assert.assertEquals(term.booleanValue(), Value.create("boolean", value));
140 | }
141 |
142 | @Test
143 | public void testValueConversionDate() throws Exception {
144 | Term term = new Term("2019-12-31 12:45:00");
145 | JsonNode value = Json.MAPPER.readTree("{\"value\":\"2019-12-31 12:45:00\"}").get("value");
146 | Assert.assertEquals(term.dateValue(), Value.create("date", value));
147 | }
148 |
149 | @Test
150 | public void testValueConversionNumberIntegerLongNegative() throws Exception {
151 | Term term = new Term("-922337203685477");
152 | JsonNode value = Json.MAPPER.readTree("{\"value\":-922337203685477}").get("value");
153 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
154 | }
155 |
156 | @Test
157 | public void testValueConversionNumberIntegerLongPositive() throws Exception {
158 | Term term = new Term("922337203685477");
159 | JsonNode value = Json.MAPPER.readTree("{\"value\":922337203685477}").get("value");
160 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
161 | }
162 |
163 | @Test
164 | public void testValueConversionNumberIntegerShortNegative() throws Exception {
165 | Term term = new Term("-1");
166 | JsonNode value = Json.MAPPER.readTree("{\"value\":-1}").get("value");
167 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
168 | }
169 |
170 | @Test
171 | public void testValueConversionNumberIntegerShortPositive() throws Exception {
172 | Term term = new Term("1");
173 | JsonNode value = Json.MAPPER.readTree("{\"value\":1}").get("value");
174 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
175 | }
176 |
177 | @Test
178 | public void testValueConversionNumberFloatLongNegative() throws Exception {
179 | Term term = new Term("-3.141592653589793");
180 | JsonNode value = Json.MAPPER.readTree("{\"value\":-3.141592653589793}").get("value");
181 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
182 | }
183 |
184 | @Test
185 | public void testValueConversionNumberFloatLongPositive() throws Exception {
186 | Term term = new Term("3.141592653589793");
187 | JsonNode value = Json.MAPPER.readTree("{\"value\":3.141592653589793}").get("value");
188 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
189 | }
190 |
191 | @Test
192 | public void testValueConversionNumberFloatShortNegative() throws Exception {
193 | Term term = new Term("-1.0");
194 | JsonNode value = Json.MAPPER.readTree("{\"value\":-1.0}").get("value");
195 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
196 | }
197 |
198 | @Test
199 | public void testValueConversionNumberFloatShortPositive() throws Exception {
200 | Term term = new Term("1.0");
201 | JsonNode value = Json.MAPPER.readTree("{\"value\":1.0}").get("value");
202 | Assert.assertEquals(term.numberValue(), Value.create("number", value));
203 | }
204 |
205 | @Test
206 | public void testValueConversionString() throws Exception {
207 | Term term = new Term("abc");
208 | JsonNode value = Json.MAPPER.readTree("{\"value\":\"abc\"}").get("value");
209 | Assert.assertEquals(term.stringValue(), Value.create("string", value));
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/test/java/org/elasticsearch/plugin/zentity/AbstractIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import org.apache.http.HttpHost;
23 | import org.elasticsearch.client.Request;
24 | import org.elasticsearch.client.Response;
25 | import org.elasticsearch.client.RestClient;
26 | import org.junit.AfterClass;
27 | import org.junit.BeforeClass;
28 | import org.junit.Ignore;
29 | import org.testcontainers.containers.DockerComposeContainer;
30 | import org.testcontainers.containers.wait.strategy.Wait;
31 |
32 | import java.io.File;
33 | import java.io.IOException;
34 | import java.nio.file.Path;
35 | import java.nio.file.Paths;
36 | import java.time.Duration;
37 |
38 | import static org.junit.Assert.*;
39 | import static org.junit.Assume.assumeFalse;
40 |
41 | @Ignore("Base class")
42 | public abstract class AbstractIT {
43 |
44 | public final static String SERVICE_NAME = "es01";
45 | public final static int SERVICE_PORT = 9400;
46 |
47 | // A docker-compose cluster used for all test cases in the test class.
48 | private static DockerComposeContainer cluster;
49 |
50 | // Client that communicates with the docker-compose cluster.
51 | private static RestClient client;
52 |
53 | @BeforeClass
54 | public static void setup() throws Exception {
55 | createCluster();
56 | createClient();
57 | }
58 |
59 | @AfterClass
60 | public static void tearDown() throws IOException {
61 | destroyClient();
62 | destroyCluster();
63 | }
64 |
65 | /**
66 | * Create the cluster if it doesn't exist.
67 | */
68 | public static void createCluster() {
69 | Path path = Paths.get(System.getenv("BUILD_DIRECTORY"), "test-classes", "docker-compose.yml");
70 | cluster = new DockerComposeContainer(new File(path.toString()))
71 | .withEnv("BUILD_DIRECTORY", System.getenv("BUILD_DIRECTORY"))
72 | .withEnv("ELASTICSEARCH_VERSION", System.getenv("ELASTICSEARCH_VERSION"))
73 | .withEnv("ZENTITY_VERSION", System.getenv("ZENTITY_VERSION"))
74 | .withExposedService(SERVICE_NAME, SERVICE_PORT,
75 | Wait.forHttp("/_cat/health")
76 | .forStatusCodeMatching(it -> it >= 200 && it < 300)
77 | .withReadTimeout(Duration.ofSeconds(120)));
78 | cluster.start();
79 | }
80 |
81 | /**
82 | * Destroy the cluster if it exists.
83 | */
84 | public static void destroyCluster() {
85 | if (cluster != null) {
86 | cluster.stop();
87 | cluster = null;
88 | }
89 | }
90 |
91 | /**
92 | * Create the client if it doesn't exist.
93 | */
94 | public static void createClient() throws IOException {
95 |
96 | // The client configuration depends on the cluster implementation,
97 | // so create the cluster first if it hasn't already been created.
98 | if (cluster == null)
99 | createCluster();
100 |
101 | try {
102 |
103 | // Create the client.
104 | String host = cluster.getServiceHost(SERVICE_NAME, SERVICE_PORT);
105 | Integer port = cluster.getServicePort(SERVICE_NAME, SERVICE_PORT);
106 | client = RestClient.builder(new HttpHost(host, port)).build();
107 |
108 | // Verify if the client can establish a connection to the cluster.
109 | Response response = client.performRequest(new Request("GET", "/"));
110 | JsonNode json = Json.MAPPER.readTree(response.getEntity().getContent());
111 | assertEquals("You Know, for Search", json.get("tagline").textValue());
112 |
113 | } catch (IOException e) {
114 |
115 | // If we have an exception here, let's ignore the test.
116 | destroyClient();
117 | assumeFalse("Integration tests are skipped", e.getMessage().contains("Connection refused"));
118 | fail("Something wrong is happening. REST Client seemed to raise an exception: " + e.getMessage());
119 | }
120 | }
121 |
122 | /**
123 | * Destroy the client if it exists.
124 | */
125 | public static void destroyClient() throws IOException {
126 | if (client != null) {
127 | client.close();
128 | client = null;
129 | }
130 | }
131 |
132 | /**
133 | * Return the client to be used in test cases.
134 | *
135 | * @return The client.
136 | */
137 | public static RestClient client() throws IOException {
138 | if (client == null)
139 | createClient();
140 | return client;
141 | }
142 | }
--------------------------------------------------------------------------------
/src/test/java/org/elasticsearch/plugin/zentity/HomeActionIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import org.elasticsearch.client.Request;
23 | import org.elasticsearch.client.Response;
24 | import org.junit.Test;
25 |
26 | import java.io.InputStream;
27 | import java.util.Properties;
28 |
29 | import static org.junit.Assert.assertEquals;
30 |
31 | public class HomeActionIT extends AbstractIT {
32 |
33 | @Test
34 | public void testHomeAction() throws Exception {
35 |
36 | // Get plugin properties
37 | Properties props = new Properties();
38 | Properties zentityProperties = new Properties();
39 | Properties pluginDescriptorProperties = new Properties();
40 | InputStream zentityStream = ZentityPlugin.class.getResourceAsStream("/zentity.properties");
41 | InputStream pluginDescriptorStream = ZentityPlugin.class.getResourceAsStream("/plugin-descriptor.properties");
42 | zentityProperties.load(zentityStream);
43 | pluginDescriptorProperties.load(pluginDescriptorStream);
44 | props.putAll(zentityProperties);
45 | props.putAll(pluginDescriptorProperties);
46 |
47 | // Verify if the plugin properties match the output of GET _zentity
48 | Response response = client().performRequest(new Request("GET", "_zentity"));
49 | JsonNode json = Json.MAPPER.readTree(response.getEntity().getContent());
50 | assertEquals(json.get("name").asText(), props.getProperty("name"));
51 | assertEquals(json.get("description").asText(), props.getProperty("description"));
52 | assertEquals(json.get("website").asText(), props.getProperty("zentity.website"));
53 | assertEquals(json.get("version").get("zentity").asText(), props.getProperty("zentity.version"));
54 | assertEquals(json.get("version").get("elasticsearch").asText(), props.getProperty("elasticsearch.version"));
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/test/java/org/elasticsearch/plugin/zentity/SetupActionIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import org.elasticsearch.client.Request;
23 | import org.elasticsearch.client.Response;
24 | import org.elasticsearch.client.ResponseException;
25 | import org.junit.Test;
26 |
27 | import static org.junit.Assert.*;
28 |
29 | public class SetupActionIT extends AbstractIT {
30 |
31 | public static void destroyTestResources() throws Exception {
32 | try {
33 | client().performRequest(new Request("DELETE", ModelsAction.INDEX_NAME));
34 | } catch (ResponseException e) {
35 | // Destroy the test index if it already exists, otherwise continue.
36 | }
37 | }
38 |
39 | @Test
40 | public void testSetupDefault() throws Exception {
41 | destroyTestResources();
42 | try {
43 |
44 | // Run setup with default settings
45 | Response setupResponse = client().performRequest(new Request("POST", "_zentity/_setup"));
46 | JsonNode setupJson = Json.MAPPER.readTree(setupResponse.getEntity().getContent());
47 |
48 | // The response should be { "acknowledged": true }
49 | JsonNode acknowledged = setupJson.get("acknowledged");
50 | assertTrue(acknowledged.isBoolean() && acknowledged.asBoolean());
51 |
52 | // Get the index settings and mapping
53 | Response getIndexResponse = client().performRequest(new Request("GET", ModelsAction.INDEX_NAME));
54 | JsonNode getIndexJson = Json.MAPPER.readTree(getIndexResponse.getEntity().getContent());
55 |
56 | // Verify if the mapping matches the default mapping
57 | JsonNode mappingJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("mappings");
58 | assertEquals(mappingJson.get("dynamic").asText(), "strict");
59 | assertEquals(mappingJson.get("properties").get("attributes").get("type").asText(), "object");
60 | assertFalse(mappingJson.get("properties").get("attributes").get("enabled").booleanValue());
61 | assertEquals(mappingJson.get("properties").get("resolvers").get("type").asText(), "object");
62 | assertFalse(mappingJson.get("properties").get("resolvers").get("enabled").booleanValue());
63 | assertEquals(mappingJson.get("properties").get("matchers").get("type").asText(), "object");
64 | assertFalse(mappingJson.get("properties").get("matchers").get("enabled").booleanValue());
65 | assertEquals(mappingJson.get("properties").get("indices").get("type").asText(), "object");
66 | assertFalse(mappingJson.get("properties").get("indices").get("enabled").booleanValue());
67 |
68 | // Verify if the settings match the default settings
69 | JsonNode settingsJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("settings");
70 | assertEquals(settingsJson.get("index").get("number_of_shards").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_SHARDS));
71 | assertEquals(settingsJson.get("index").get("number_of_replicas").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_REPLICAS));
72 |
73 | } finally {
74 | destroyTestResources();
75 | }
76 | }
77 |
78 | @Test
79 | public void testSetupCustom() throws Exception {
80 | destroyTestResources();
81 | try {
82 |
83 | // Run setup with custom settings
84 | Response setupResponse = client().performRequest(new Request("POST", "_zentity/_setup?number_of_shards=2&number_of_replicas=2"));
85 | JsonNode setupJson = Json.MAPPER.readTree(setupResponse.getEntity().getContent());
86 |
87 | // The response should be { "acknowledged": true }
88 | JsonNode acknowledged = setupJson.get("acknowledged");
89 | assertTrue(acknowledged.isBoolean() && acknowledged.asBoolean());
90 |
91 | // Get the index settings and mapping
92 | Response getIndexResponse = client().performRequest(new Request("GET", ModelsAction.INDEX_NAME));
93 | JsonNode getIndexJson = Json.MAPPER.readTree(getIndexResponse.getEntity().getContent());
94 |
95 | // Verify if the settings match the default settings
96 | JsonNode settingsJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("settings");
97 | assertEquals(settingsJson.get("index").get("number_of_shards").asText(), "2");
98 | assertEquals(settingsJson.get("index").get("number_of_replicas").asText(), "2");
99 |
100 | } finally {
101 | destroyTestResources();
102 | }
103 | }
104 |
105 | @Test
106 | public void testSetupDeconflict() throws Exception {
107 | destroyTestResources();
108 | try {
109 |
110 | // Run setup with default settings
111 | Response setupDefaultResponse = client().performRequest(new Request("POST", "_zentity/_setup"));
112 | JsonNode setupDefaultJson = Json.MAPPER.readTree(setupDefaultResponse.getEntity().getContent());
113 |
114 | // The response should be { "acknowledged": true }
115 | JsonNode acknowledged = setupDefaultJson.get("acknowledged");
116 | assertTrue(acknowledged.isBoolean() && acknowledged.asBoolean());
117 |
118 | // Run setup again with custom settings
119 | try {
120 | client().performRequest(new Request("POST", "_zentity/_setup?number_of_shards=2&number_of_replicas=2"));
121 | } catch (ResponseException e) {
122 |
123 | // The response should be an error
124 | JsonNode setupCustomJson = Json.MAPPER.readTree(e.getResponse().getEntity().getContent());
125 | assertEquals(e.getResponse().getStatusLine().getStatusCode(), 400);
126 | assertEquals(setupCustomJson.get("error").get("type").asText(), "resource_already_exists_exception");
127 | }
128 |
129 | // Get the index settings and mapping
130 | Response getIndexResponse = client().performRequest(new Request("GET", ModelsAction.INDEX_NAME));
131 | JsonNode getIndexJson = Json.MAPPER.readTree(getIndexResponse.getEntity().getContent());
132 |
133 | // Verify if the settings match the default settings and not the custom settings
134 | JsonNode settingsJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("settings");
135 | assertEquals(settingsJson.get("index").get("number_of_shards").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_SHARDS));
136 | assertEquals(settingsJson.get("index").get("number_of_replicas").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_REPLICAS));
137 |
138 | } finally {
139 | destroyTestResources();
140 | }
141 | }
142 | }
--------------------------------------------------------------------------------
/src/test/java/org/elasticsearch/plugin/zentity/ZentityPluginIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * zentity
3 | * Copyright © 2018-2025 Dave Moore
4 | * https://zentity.io
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package org.elasticsearch.plugin.zentity;
19 |
20 | import com.fasterxml.jackson.databind.JsonNode;
21 | import io.zentity.common.Json;
22 | import org.elasticsearch.client.Request;
23 | import org.elasticsearch.client.Response;
24 | import org.junit.Test;
25 |
26 | import java.util.Iterator;
27 | import java.util.Map;
28 |
29 | import static org.junit.Assert.assertTrue;
30 |
31 | public class ZentityPluginIT extends AbstractIT {
32 |
33 | @Test
34 | public void testPluginIsLoaded() throws Exception {
35 | Response response = client().performRequest(new Request("GET", "_nodes/plugins"));
36 | JsonNode json = Json.MAPPER.readTree(response.getEntity().getContent());
37 | Iterator> nodes = json.get("nodes").fields();
38 | while (nodes.hasNext()) {
39 | Map.Entry entry = nodes.next();
40 | JsonNode node = entry.getValue();
41 | boolean pluginFound = false;
42 | for (JsonNode plugin : node.get("plugins")) {
43 | String pluginName = plugin.get("name").textValue();
44 | if (pluginName.equals("zentity")) {
45 | pluginFound = true;
46 | break;
47 | }
48 | }
49 | assertTrue(pluginFound);
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/src/test/resources/TestDataArrays.txt:
--------------------------------------------------------------------------------
1 | { "index" : { "_index" : "zentity_test_index_arrays", "_id" : "1" }}
2 | { "string" : "abc", "array_1": [ "111" ], "array_2" : ["222",null,"222"], "array_3" : [],"array_4" : [ "222", "333", "444" ]}
3 | { "index" : { "_index" : "zentity_test_index_arrays", "_id" : "2" }}
4 | { "string" : "xyz", "array_1": [ "444" ], "array_2" : null, "array_4" : [ "555" ]}
5 |
--------------------------------------------------------------------------------
/src/test/resources/TestDataObjectArrays.txt:
--------------------------------------------------------------------------------
1 | { "index" : { "_index" : "zentity_test_index_object_arrays", "_id": "1" }}
2 | { "first_name" : "alice", "last_name" : "jones", "phone" : [{ "number" : "555-123-4567", "type" : "home" }, { "number" : "555-987-6543", "type" : "mobile" }]}
3 | { "index" : { "_index" : "zentity_test_index_object_arrays", "_id": "2" }}
4 | { "first_name" : "allison", "last_name" : "jones", "phone" : [{ "number" : "555-987-6543", "type" : "mobile" }]}
5 |
--------------------------------------------------------------------------------
/src/test/resources/TestEntityModelArrays.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "string": {},
4 | "array": {}
5 | },
6 | "resolvers": {
7 | "string": {
8 | "attributes": [
9 | "string"
10 | ]
11 | },
12 | "array": {
13 | "attributes": [
14 | "array"
15 | ]
16 | }
17 | },
18 | "matchers": {
19 | "exact": {
20 | "clause": {
21 | "term": {
22 | "{{ field }}": "{{ value }}"
23 | }
24 | }
25 | }
26 | },
27 | "indices": {
28 | "zentity_test_index_arrays": {
29 | "fields": {
30 | "string": {
31 | "attribute": "string",
32 | "matcher": "exact"
33 | },
34 | "array_1": {
35 | "attribute": "array",
36 | "matcher": "exact"
37 | },
38 | "array_2": {
39 | "attribute": "array",
40 | "matcher": "exact"
41 | },
42 | "array_3": {
43 | "attribute": "array",
44 | "matcher": "exact"
45 | },
46 | "array_4": {
47 | "attribute": "array",
48 | "matcher": "exact"
49 | }
50 | }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/test/resources/TestEntityModelB.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "attribute_a": {
4 | "type": "string"
5 | },
6 | "attribute_b": {
7 | "type": "string"
8 | },
9 | "attribute_c": {
10 | "type": "string"
11 | },
12 | "attribute_d": {
13 | "type": "string"
14 | },
15 | "attribute_x": {
16 | "type": "string"
17 | },
18 | "attribute_unused": {
19 | "type": "string"
20 | }
21 | },
22 | "resolvers": {
23 | "resolver_ab": {
24 | "attributes": [
25 | "attribute_a", "attribute_b"
26 | ],
27 | "weight": -1
28 | },
29 | "resolver_ac": {
30 | "attributes": [
31 | "attribute_a", "attribute_c"
32 | ],
33 | "weight": -1
34 | },
35 | "resolver_bc": {
36 | "attributes": [
37 | "attribute_b", "attribute_c"
38 | ],
39 | "weight": 1
40 | },
41 | "resolver_cd": {
42 | "attributes": [
43 | "attribute_c", "attribute_d"
44 | ],
45 | "weight": -1
46 | },
47 | "resolver_x": {
48 | "attributes": [
49 | "attribute_x"
50 | ]
51 | },
52 | "resolver_unused": {
53 | "attributes": [
54 | "attribute_unused"
55 | ]
56 | }
57 | },
58 | "matchers": {
59 | "matcher_a": {
60 | "clause": {
61 | "match": {
62 | "{{ field }}": "{{ value }}"
63 | }
64 | }
65 | },
66 | "matcher_b": {
67 | "clause": {
68 | "term": {
69 | "{{ field }}": "{{ value }}"
70 | }
71 | }
72 | }
73 | },
74 | "indices": {
75 | "zentity_test_index_a": {
76 | "fields": {
77 | "field_a.clean": {
78 | "attribute": "attribute_a",
79 | "matcher": "matcher_a"
80 | },
81 | "field_b.clean": {
82 | "attribute": "attribute_b",
83 | "matcher": "matcher_a"
84 | },
85 | "field_c.clean": {
86 | "attribute": "attribute_c",
87 | "matcher": "matcher_a"
88 | },
89 | "field_d.clean": {
90 | "attribute": "attribute_d",
91 | "matcher": "matcher_a"
92 | },
93 | "object.a.b.c.keyword": {
94 | "attribute": "attribute_x",
95 | "matcher": "matcher_b"
96 | },
97 | "unused": {
98 | "attribute": "attribute_unused",
99 | "matcher": "matcher_b"
100 | },
101 | "unused_matcher": {
102 | "attribute": "attribute_unused"
103 | },
104 | "unused_matcher_null": {
105 | "attribute": "attribute_unused",
106 | "matcher": null
107 | }
108 | }
109 | }
110 | }
111 | }
--------------------------------------------------------------------------------
/src/test/resources/TestEntityModelElasticsearchError.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "attribute_a": {
4 | "type": "string"
5 | },
6 | "attribute_b": {
7 | "type": "string"
8 | },
9 | "attribute_c": {
10 | "type": "string"
11 | },
12 | "attribute_d": {
13 | "type": "string"
14 | },
15 | "attribute_x": {
16 | "type": "string"
17 | },
18 | "attribute_unused": {
19 | "type": "string"
20 | }
21 | },
22 | "resolvers": {
23 | "resolver_ab": {
24 | "attributes": [
25 | "attribute_a", "attribute_b"
26 | ],
27 | "weight": -1
28 | },
29 | "resolver_ac": {
30 | "attributes": [
31 | "attribute_a", "attribute_c"
32 | ],
33 | "weight": -1
34 | },
35 | "resolver_bc": {
36 | "attributes": [
37 | "attribute_b", "attribute_c"
38 | ],
39 | "weight": 1
40 | },
41 | "resolver_cd": {
42 | "attributes": [
43 | "attribute_c", "attribute_d"
44 | ],
45 | "weight": -1
46 | },
47 | "resolver_x": {
48 | "attributes": [
49 | "attribute_x"
50 | ]
51 | },
52 | "resolver_unused": {
53 | "attributes": [
54 | "attribute_unused"
55 | ]
56 | }
57 | },
58 | "matchers": {
59 | "matcher_a": {
60 | "clause": {
61 | "match": {
62 | "{{ field }}": "{{ value }}"
63 | }
64 | }
65 | },
66 | "matcher_b": {
67 | "clause": {
68 | "example_malformed_query": {
69 | "{{ field }}": "{{ value }}"
70 | }
71 | }
72 | }
73 | },
74 | "indices": {
75 | "zentity_test_index_a": {
76 | "fields": {
77 | "field_a.clean": {
78 | "attribute": "attribute_a",
79 | "matcher": "matcher_a"
80 | },
81 | "field_b.clean": {
82 | "attribute": "attribute_b",
83 | "matcher": "matcher_a"
84 | },
85 | "field_c.clean": {
86 | "attribute": "attribute_c",
87 | "matcher": "matcher_a"
88 | },
89 | "field_d.clean": {
90 | "attribute": "attribute_d",
91 | "matcher": "matcher_a"
92 | },
93 | "object.a.b.c.keyword": {
94 | "attribute": "attribute_x",
95 | "matcher": "matcher_b"
96 | },
97 | "unused": {
98 | "attribute": "attribute_unused",
99 | "matcher": "matcher_b"
100 | }
101 | }
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/src/test/resources/TestEntityModelObjectArrays.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "first_name": {},
4 | "last_name": {},
5 | "phone": {}
6 | },
7 | "resolvers": {
8 | "name_phone": {
9 | "attributes": [
10 | "last_name",
11 | "phone"
12 | ]
13 | }
14 | },
15 | "matchers": {
16 | "exact": {
17 | "clause": {
18 | "term": {
19 | "{{ field }}": "{{ value }}"
20 | }
21 | }
22 | },
23 | "exact_phone": {
24 | "clause": {
25 | "nested": {
26 | "path": "phone",
27 | "query": {
28 | "term": {
29 | "{{ field }}": "{{ value }}"
30 | }
31 | }
32 | }
33 | }
34 | }
35 | },
36 | "indices": {
37 | "zentity_test_index_object_arrays": {
38 | "fields": {
39 | "first_name": {
40 | "attribute": "first_name",
41 | "matcher": "exact"
42 | },
43 | "last_name": {
44 | "attribute": "last_name",
45 | "matcher": "exact"
46 | },
47 | "phone.number": {
48 | "attribute": "phone",
49 | "matcher": "exact_phone"
50 | }
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/test/resources/TestEntityModelZentityError.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "attribute_a": {
4 | "type": "string"
5 | },
6 | "attribute_b": {
7 | "type": "string"
8 | },
9 | "attribute_c": {
10 | "type": "string"
11 | },
12 | "attribute_d": {
13 | "type": "string"
14 | },
15 | "attribute_x": {
16 | "type": "number"
17 | },
18 | "attribute_unused": {
19 | "type": "string"
20 | }
21 | },
22 | "resolvers": {
23 | "resolver_ab": {
24 | "attributes": [
25 | "attribute_a", "attribute_b"
26 | ],
27 | "weight": -1
28 | },
29 | "resolver_ac": {
30 | "attributes": [
31 | "attribute_a", "attribute_c"
32 | ],
33 | "weight": -1
34 | },
35 | "resolver_bc": {
36 | "attributes": [
37 | "attribute_b", "attribute_c"
38 | ],
39 | "weight": 1
40 | },
41 | "resolver_cd": {
42 | "attributes": [
43 | "attribute_c", "attribute_d"
44 | ],
45 | "weight": -1
46 | },
47 | "resolver_x": {
48 | "attributes": [
49 | "attribute_x"
50 | ]
51 | },
52 | "resolver_unused": {
53 | "attributes": [
54 | "attribute_unused"
55 | ]
56 | }
57 | },
58 | "matchers": {
59 | "matcher_a": {
60 | "clause": {
61 | "match": {
62 | "{{ field }}": "{{ value }}"
63 | }
64 | }
65 | },
66 | "matcher_b": {
67 | "clause": {
68 | "term": {
69 | "{{ field }}": "{{ value }}"
70 | }
71 | }
72 | }
73 | },
74 | "indices": {
75 | "zentity_test_index_a": {
76 | "fields": {
77 | "field_a.clean": {
78 | "attribute": "attribute_a",
79 | "matcher": "matcher_a"
80 | },
81 | "field_b.clean": {
82 | "attribute": "attribute_b",
83 | "matcher": "matcher_a"
84 | },
85 | "field_c.clean": {
86 | "attribute": "attribute_c",
87 | "matcher": "matcher_a"
88 | },
89 | "field_d.clean": {
90 | "attribute": "attribute_d",
91 | "matcher": "matcher_a"
92 | },
93 | "object.a.b.c.keyword": {
94 | "attribute": "attribute_x",
95 | "matcher": "matcher_b"
96 | },
97 | "unused": {
98 | "attribute": "attribute_unused",
99 | "matcher": "matcher_b"
100 | }
101 | }
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/src/test/resources/TestIndex.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "number_of_shards": 1,
4 | "number_of_replicas": 2,
5 | "analysis": {
6 | "filter": {
7 | "strip_punct": {
8 | "type": "pattern_replace",
9 | "pattern": "[^a-zA-Z0-9]",
10 | "replacement": ""
11 | }
12 | },
13 | "analyzer": {
14 | "strip_punct": {
15 | "filter": [
16 | "strip_punct"
17 | ],
18 | "tokenizer": "keyword"
19 | }
20 | }
21 | }
22 | },
23 | "mappings": {
24 | "properties": {
25 | "field_a": {
26 | "type": "text",
27 | "fields": {
28 | "clean": {
29 | "type": "text",
30 | "analyzer": "strip_punct"
31 | },
32 | "keyword": {
33 | "type": "keyword"
34 | }
35 | }
36 | },
37 | "field_b": {
38 | "type": "text",
39 | "fields": {
40 | "clean": {
41 | "type": "text",
42 | "analyzer": "strip_punct"
43 | },
44 | "keyword": {
45 | "type": "keyword"
46 | }
47 | }
48 | },
49 | "field_c": {
50 | "type": "text",
51 | "fields": {
52 | "clean": {
53 | "type": "text",
54 | "analyzer": "strip_punct"
55 | },
56 | "keyword": {
57 | "type": "keyword"
58 | }
59 | }
60 | },
61 | "field_d": {
62 | "type": "text",
63 | "fields": {
64 | "clean": {
65 | "type": "text",
66 | "analyzer": "strip_punct"
67 | },
68 | "keyword": {
69 | "type": "keyword"
70 | }
71 | }
72 | },
73 | "type_boolean": {
74 | "type": "boolean"
75 | },
76 | "type_date": {
77 | "type": "date",
78 | "format": "yyyy-MM-dd'T'HH:mm:ss.SSS"
79 | },
80 | "type_double": {
81 | "type": "double"
82 | },
83 | "type_float": {
84 | "type": "float"
85 | },
86 | "type_integer": {
87 | "type": "integer"
88 | },
89 | "type_long": {
90 | "type": "long"
91 | },
92 | "type_string": {
93 | "type": "keyword"
94 | },
95 | "type_string_null": {
96 | "type": "keyword"
97 | },
98 | "object": {
99 | "properties": {
100 | "a": {
101 | "properties": {
102 | "b": {
103 | "properties": {
104 | "c": {
105 | "type": "text",
106 | "fields": {
107 | "keyword": {
108 | "type": "keyword"
109 | }
110 | }
111 | }
112 | }
113 | }
114 | }
115 | }
116 | }
117 | },
118 | "unused": {
119 | "type": "keyword"
120 | }
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/src/test/resources/TestIndexArrays.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "number_of_shards": 1,
4 | "number_of_replicas": 2
5 | },
6 | "mappings" : {
7 | "properties" : {
8 | "array_1" : {
9 | "type" : "keyword"
10 | },
11 | "array_2" : {
12 | "type" : "keyword"
13 | },
14 | "array_3" : {
15 | "type" : "keyword"
16 | },
17 | "array_4" : {
18 | "type" : "keyword"
19 | },
20 | "string" : {
21 | "type" : "keyword"
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/test/resources/TestIndexObjectArrays.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "number_of_shards": 1,
4 | "number_of_replicas": 2
5 | },
6 | "mappings": {
7 | "properties": {
8 | "first_name": {
9 | "type": "text"
10 | },
11 | "last_name": {
12 | "type": "text"
13 | },
14 | "phone": {
15 | "type": "nested",
16 | "properties": {
17 | "number": {
18 | "type": "keyword"
19 | },
20 | "type": {
21 | "type": "keyword"
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/test/resources/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | es01:
4 | image: "docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}"
5 | user: elasticsearch
6 | command:
7 | - /bin/bash
8 | - -c
9 | - "elasticsearch-plugin install --batch file:///releases/zentity-${ZENTITY_VERSION}-elasticsearch-${ELASTICSEARCH_VERSION}.zip && elasticsearch"
10 | environment:
11 | - node.name=es01
12 | - cluster.name=zentity-test-cluster
13 | - cluster.initial_master_nodes=es01
14 | #- cluster.initial_master_nodes=es01,es02,es03
15 | #- discovery.seed_hosts=es02,es03
16 | - http.port=9400
17 | - bootstrap.memory_lock=true
18 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m -ea"
19 | - xpack.security.enabled=false
20 | - action.destructive_requires_name=false
21 | ulimits:
22 | memlock:
23 | soft: -1
24 | hard: -1
25 | volumes:
26 | - data01:/usr/share/elasticsearch/data
27 | - ${BUILD_DIRECTORY}/releases/:/releases
28 | ports:
29 | - 9400:9400
30 | networks:
31 | - elastic
32 | # es02:
33 | # image: "docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}"
34 | # user: elasticsearch
35 | # command:
36 | # - /bin/bash
37 | # - -c
38 | # - "elasticsearch-plugin install --batch file:///releases/zentity-${ZENTITY_VERSION}-elasticsearch-${ELASTICSEARCH_VERSION}.zip && elasticsearch"
39 | # environment:
40 | # - node.name=es02
41 | # - cluster.name=zentity-test-cluster
42 | # - cluster.initial_master_nodes=es01,es02,es03
43 | # - discovery.seed_hosts=es01,es03
44 | # - http.port=9400
45 | # - bootstrap.memory_lock=true
46 | # - "ES_JAVA_OPTS=-Xms512m -Xmx512m -ea"
47 | # - xpack.security.enabled=false
48 | # - action.destructive_requires_name=false
49 | # ulimits:
50 | # memlock:
51 | # soft: -1
52 | # hard: -1
53 | # volumes:
54 | # - data02:/usr/share/elasticsearch/data
55 | # - ${BUILD_DIRECTORY}/releases/:/releases
56 | # networks:
57 | # - elastic
58 | # es03:
59 | # image: "docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}"
60 | # user: elasticsearch
61 | # command:
62 | # - /bin/bash
63 | # - -c
64 | # - "elasticsearch-plugin install --batch file:///releases/zentity-${ZENTITY_VERSION}-elasticsearch-${ELASTICSEARCH_VERSION}.zip && elasticsearch"
65 | # environment:
66 | # - node.name=es03
67 | # - cluster.name=zentity-test-cluster
68 | # - cluster.initial_master_nodes=es01,es02,es03
69 | # - discovery.seed_hosts=es01,es02
70 | # - http.port=9400
71 | # - bootstrap.memory_lock=true
72 | # - "ES_JAVA_OPTS=-Xms512m -Xmx512m -ea"
73 | # - xpack.security.enabled=false
74 | # - action.destructive_requires_name=false
75 | # ulimits:
76 | # memlock:
77 | # soft: -1
78 | # hard: -1
79 | # volumes:
80 | # - data03:/usr/share/elasticsearch/data
81 | # - ${BUILD_DIRECTORY}/releases/:/releases
82 | # networks:
83 | # - elastic
84 |
85 | volumes:
86 | data01:
87 | driver: local
88 | # data02:
89 | # driver: local
90 | # data03:
91 | # driver: local
92 |
93 | networks:
94 | elastic:
95 | driver: bridge
--------------------------------------------------------------------------------
/src/test/resources/log4j.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------