Skip to content

Commit ceaee99

Browse files
author
Philip Z
committed
Warn instead of failing on stale dynamic search fields
Closes #74.
1 parent 2bcc027 commit ceaee99

6 files changed

Lines changed: 236 additions & 1 deletion

File tree

‎teaql-core/src/main/java/io/teaql/core/BaseRequest.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ public SearchCriteria getSearchCriteria() {
205205
return searchCriteria;
206206
}
207207

208+
/**
209+
* Replaces the current criteria as one atomic parser operation. Dynamic input adapters use
210+
* this to roll back a partially resolved relation path when a later field segment is stale.
211+
*/
212+
public void replaceSearchCriteria(SearchCriteria searchCriteria) {
213+
this.searchCriteria = searchCriteria;
214+
}
215+
208216
@Override
209217
public OrderBys getOrderBy() {
210218
return orderBys;

‎teaql-query-json/pom.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,11 @@
2323
<groupId>com.fasterxml.jackson.core</groupId>
2424
<artifactId>jackson-databind</artifactId>
2525
</dependency>
26+
<dependency>
27+
<groupId>junit</groupId>
28+
<artifactId>junit</artifactId>
29+
<version>4.13.2</version>
30+
<scope>test</scope>
31+
</dependency>
2632
</dependencies>
2733
</project>

‎teaql-query-json/src/main/java/io/teaql/query/json/DynamicSearchHelper.java‎

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
import java.util.Date;
44
import java.util.Iterator;
5+
import java.util.ArrayList;
6+
import java.util.Collections;
7+
import java.util.List;
58
import java.util.Map;
69
import java.util.Optional;
710
import java.util.concurrent.atomic.AtomicInteger;
11+
import java.util.logging.Level;
12+
import java.util.logging.Logger;
813
import com.fasterxml.jackson.databind.JsonNode;
914
import com.fasterxml.jackson.databind.ObjectMapper;
1015
import com.fasterxml.jackson.databind.node.JsonNodeType;
@@ -68,6 +73,53 @@ public void setDateTimeField(boolean dateTimeField) {
6873

6974
public class DynamicSearchHelper {
7075

76+
public static final String WARNINGS_EXTENSION = "teaql.dynamicSearch.warnings";
77+
private static final Logger LOGGER = Logger.getLogger(DynamicSearchHelper.class.getName());
78+
79+
public static List<DynamicSearchWarning> warningsOf(BaseRequest request) {
80+
Object warnings = request.getExtensions().get(WARNINGS_EXTENSION);
81+
if (!(warnings instanceof List<?>)) {
82+
return Collections.emptyList();
83+
}
84+
List<DynamicSearchWarning> result = new ArrayList<>();
85+
for (Object warning : (List<?>) warnings) {
86+
if (warning instanceof DynamicSearchWarning) {
87+
result.add((DynamicSearchWarning) warning);
88+
}
89+
}
90+
return Collections.unmodifiableList(result);
91+
}
92+
93+
protected void warnUnknownField(BaseRequest request, String clause, String fieldPath) {
94+
DynamicSearchWarning warning =
95+
new DynamicSearchWarning(
96+
DynamicSearchWarning.UNKNOWN_FIELD,
97+
request.getTypeName(),
98+
clause,
99+
fieldPath);
100+
Object existing = request.getExtensions().get(WARNINGS_EXTENSION);
101+
List<DynamicSearchWarning> warnings;
102+
if (existing instanceof List<?>) {
103+
@SuppressWarnings("unchecked")
104+
List<DynamicSearchWarning> typed = (List<DynamicSearchWarning>) existing;
105+
warnings = typed;
106+
}
107+
else {
108+
warnings = new ArrayList<>();
109+
request.putExtension(WARNINGS_EXTENSION, warnings);
110+
}
111+
warnings.add(warning);
112+
LOGGER.log(
113+
Level.WARNING,
114+
"Ignored unknown dynamic search field: code={0}, requestType={1}, clause={2}, fieldPath={3}",
115+
new Object[] {
116+
warning.getCode(),
117+
warning.getRequestType(),
118+
warning.getClause(),
119+
warning.getFieldPath()
120+
});
121+
}
122+
71123
protected static JsonNode jsonFromString(String jsonExpr) {
72124
try {
73125
ObjectMapper objectMapper = new ObjectMapper();
@@ -129,6 +181,9 @@ public void addJsonFilter(BaseRequest baseRequest, JsonNode jsonNode) {
129181
String fieldName = field.getKey();
130182

131183
if (!baseRequest.isOneOfSelfField(fieldName)) {
184+
if (!fieldName.startsWith("_")) {
185+
warnUnknownField(baseRequest, "FILTER", fieldName);
186+
}
132187
continue;
133188
}
134189
JsonNode fieldValue = field.getValue();
@@ -155,12 +210,31 @@ protected boolean handleChainField(
155210
if (fieldNames.length < 2) {
156211
return true; // need to continue
157212
}
213+
SearchCriteria originalCriteria = rootRequest.getSearchCriteria();
158214
BaseRequest currentRequest = rootRequest;
159215
for (int i = 0; i < fieldNames.length - 1; i++) {
160-
Optional<BaseRequest> optional = currentRequest.subRequestOfFieldName(fieldNames[i]);
216+
Optional<BaseRequest> optional;
217+
try {
218+
optional = currentRequest.subRequestOfFieldName(fieldNames[i]);
219+
}
220+
catch (IllegalArgumentException exception) {
221+
rootRequest.replaceSearchCriteria(originalCriteria);
222+
warnUnknownField(rootRequest, "FILTER", fieldName);
223+
return false;
224+
}
225+
if (optional.isEmpty()) {
226+
rootRequest.replaceSearchCriteria(originalCriteria);
227+
warnUnknownField(rootRequest, "FILTER", fieldName);
228+
return false;
229+
}
161230
currentRequest = optional.get();
162231
}
163232
final String lastSegmentOfField = fieldNames[fieldNames.length - 1];
233+
if (!currentRequest.isOneOfSelfField(lastSegmentOfField)) {
234+
rootRequest.replaceSearchCriteria(originalCriteria);
235+
warnUnknownField(rootRequest, "FILTER", fieldName);
236+
return false;
237+
}
164238
// last segment of field, use it as value
165239
currentRequest.appendSearchCriteria(
166240
currentRequest.createBasicSearchCriteria(
@@ -356,6 +430,7 @@ public void addJsonOrderBy(BaseRequest baseRequest, JsonNode jsonNode) {
356430
// single text
357431
if (fieldValue.isTextual()) {
358432
if (!baseRequest.isOneOfSelfField(fieldValue.asText())) {
433+
warnUnknownField(baseRequest, "ORDER_BY", fieldValue.asText());
359434
return;
360435
}
361436
this.addOrderBy(baseRequest, fieldValue.asText(), false);
@@ -381,6 +456,7 @@ public void addJsonOrderBy(BaseRequest baseRequest, JsonNode jsonNode) {
381456
protected void addSingleJsonOrderBy(BaseRequest baseRequest, JsonNode jsonValueNode) {
382457
String field = jsonValueNode.get("field").asText();
383458
if (!baseRequest.isOneOfSelfField(field)) {
459+
warnUnknownField(baseRequest, "ORDER_BY", field);
384460
return;
385461
}
386462
Boolean useAsc = jsonValueNode.get("useAsc").booleanValue();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.teaql.query.json;
2+
3+
import java.util.Objects;
4+
5+
/** A non-fatal compatibility warning produced while applying a dynamic search payload. */
6+
public final class DynamicSearchWarning {
7+
8+
public static final String UNKNOWN_FIELD = "DYNAMIC_SEARCH_UNKNOWN_FIELD";
9+
10+
private final String code;
11+
private final String requestType;
12+
private final String clause;
13+
private final String fieldPath;
14+
15+
public DynamicSearchWarning(
16+
String code, String requestType, String clause, String fieldPath) {
17+
this.code = Objects.requireNonNull(code);
18+
this.requestType = Objects.requireNonNull(requestType);
19+
this.clause = Objects.requireNonNull(clause);
20+
this.fieldPath = Objects.requireNonNull(fieldPath);
21+
}
22+
23+
public String getCode() {
24+
return code;
25+
}
26+
27+
public String getRequestType() {
28+
return requestType;
29+
}
30+
31+
public String getClause() {
32+
return clause;
33+
}
34+
35+
public String getFieldPath() {
36+
return fieldPath;
37+
}
38+
39+
@Override
40+
public String toString() {
41+
return "DynamicSearchWarning{" +
42+
"code='" + code + '\'' +
43+
", requestType='" + requestType + '\'' +
44+
", clause='" + clause + '\'' +
45+
", fieldPath='" + fieldPath + '\'' +
46+
'}';
47+
}
48+
}

‎teaql-query-json/src/main/java/module-info.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
requires io.teaql.core;
33
requires io.teaql.utils;
44
requires com.fasterxml.jackson.databind;
5+
requires java.logging;
56

67
exports io.teaql.query.json;
78
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package io.teaql.query.json;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import io.teaql.core.BaseRequest;
7+
import io.teaql.core.Entity;
8+
import java.util.Optional;
9+
import org.junit.Test;
10+
11+
public class DynamicSearchHelperTest {
12+
13+
@Test
14+
public void unknownTopLevelFilterIsIgnoredAndRecorded() {
15+
StubRequest request = new StubRequest("Order");
16+
17+
new DynamicSearchHelper()
18+
.mergeClauses(
19+
request,
20+
DynamicSearchHelper.jsonFromString("{\"removedField\":\"value\"}"));
21+
22+
assertTrue(request.getSearchCriteria() == null);
23+
assertWarning(request, "FILTER", "removedField");
24+
}
25+
26+
@Test
27+
public void unknownRelationOrNestedFieldDoesNotThrow() {
28+
StubRequest request = new StubRequest("Order");
29+
request.child = new StubRequest("Customer");
30+
31+
DynamicSearchHelper helper = new DynamicSearchHelper();
32+
helper.mergeClauses(
33+
request,
34+
DynamicSearchHelper.jsonFromString("{\"removedRelation.name\":\"x\"}"));
35+
helper.mergeClauses(
36+
request,
37+
DynamicSearchHelper.jsonFromString("{\"customer.removedField\":\"x\"}"));
38+
39+
assertEquals(2, DynamicSearchHelper.warningsOf(request).size());
40+
assertTrue("stale nested fields must not leave a partial relation filter", request.getSearchCriteria() == null);
41+
assertWarning(request, "FILTER", "removedRelation.name");
42+
assertWarning(request, "FILTER", "customer.removedField");
43+
}
44+
45+
@Test
46+
public void unknownOrderFieldsAreIgnoredAndRecorded() {
47+
StubRequest request = new StubRequest("Order");
48+
49+
new DynamicSearchHelper()
50+
.mergeClauses(
51+
request,
52+
DynamicSearchHelper.jsonFromString(
53+
"{\"_orderBy\":[{\"field\":\"oldName\",\"useAsc\":true}]}"));
54+
55+
assertTrue(request.getOrderBy().isEmpty());
56+
assertWarning(request, "ORDER_BY", "oldName");
57+
}
58+
59+
private static void assertWarning(BaseRequest<?> request, String clause, String fieldPath) {
60+
assertTrue(
61+
DynamicSearchHelper.warningsOf(request).stream()
62+
.anyMatch(
63+
warning ->
64+
DynamicSearchWarning.UNKNOWN_FIELD.equals(warning.getCode())
65+
&& clause.equals(warning.getClause())
66+
&& fieldPath.equals(warning.getFieldPath())));
67+
}
68+
69+
private static final class StubRequest extends BaseRequest<Entity> {
70+
private final String typeName;
71+
private StubRequest child;
72+
73+
private StubRequest(String typeName) {
74+
super(Entity.class);
75+
this.typeName = typeName;
76+
}
77+
78+
@Override
79+
public String getTypeName() {
80+
return typeName;
81+
}
82+
83+
@Override
84+
public boolean isOneOfSelfField(String propertyName) {
85+
return "id".equals(propertyName) || "name".equals(propertyName);
86+
}
87+
88+
@Override
89+
public Optional<BaseRequest> subRequestOfFieldName(String fieldName) {
90+
if ("customer".equals(fieldName) && child != null) {
91+
return Optional.of(child);
92+
}
93+
throw new IllegalArgumentException("unknown relation");
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)