-
Notifications
You must be signed in to change notification settings - Fork 12
/
JsonPathParser.java
155 lines (144 loc) · 6.85 KB
/
JsonPathParser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package dev.blaauwendraad.masker.json.path;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
/**
* Parses a jsonpath literal into a {@link dev.blaauwendraad.masker.json.path.JsonPath} object.
* <p>
* The following features from jsonpath specification are not supported:
* <ul>
* <li>Descendant segments</li>
* <li>Child segments</li>
* <li>Name selectors</li>
* <li>Array slice selectors</li>
* <li>Index selectors</li>
* <li>Filter selectors</li>
* <li>Function extensions</li>
* <li>Escape characters</li>
* </ul>
* <p>
* The parser makes a couple of additional restrictions:
* <ul>
* <li>Numbers as key names are disallowed</li>
* <li>A set of input jsonpath literals must not be ambiguous</li>
* </ul>
* An example of ambiguous set of queries is {@code $.*.b} and {@code $.a.b}. In this case, we cannot match forward the segments.
*/
public class JsonPathParser {
private static final String ERROR_PREFIX = "Invalid jsonpath expression '%s'. ";
/**
* Parses an input literal into a {@link dev.blaauwendraad.masker.json.path.JsonPath} object.
* Throws {@link java.lang.IllegalArgumentException} when the input literal does not follow the jsonpath specification.
*
* @param literal a jsonpath literal to be parsed.
* @return {@link dev.blaauwendraad.masker.json.path.JsonPath} object parsed from the literal.
*/
@Nonnull
public JsonPath parse(String literal) {
if (!(literal.equals("$") || literal.startsWith("$.") || literal.startsWith("$["))) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "JSONPath must start with a root node identifier.");
}
if (literal.contains("'") || literal.contains("\\")) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "Escape characters are not supported.");
}
if (literal.contains("..")) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "Descendant segments are not supported.");
}
List<String> segments = parseSegments(literal);
segments.forEach(segment -> validateSegment(segment, literal));
return new JsonPath(segments.toArray(String[]::new));
}
/**
* Parses an input literal into a {@link dev.blaauwendraad.masker.json.path.JsonPath} object.
* Returns null when the input literal does not follow the jsonpath specification.
*
* @param literal a jsonpath literal to be parsed.
* @return a {@link dev.blaauwendraad.masker.json.path.JsonPath} object parsed from the literal.
*/
public JsonPath tryParse(String literal) {
try {
return parse(literal);
} catch (IllegalArgumentException ignore) {
return null;
}
}
private List<String> parseSegments(String literal) {
List<String> segments = new ArrayList<>();
segments.add("$");
if (literal.equals("$")) {
return segments;
}
StringBuilder segment = new StringBuilder();
for (int i = 2; i < literal.length() - 1; i++) {
char symbol = literal.charAt(i);
char nextSymbol = literal.charAt(i + 1);
if (symbol == '.' || (symbol == '[' && !segment.isEmpty())) {
segments.add(segment.toString());
segment = new StringBuilder();
} else if ((symbol == ']' && nextSymbol == '.') || (symbol == ']' && nextSymbol == '[')) {
segments.add(segment.toString());
segment = new StringBuilder();
i++; // NOSONAR this statement skips the next segment delimiter symbol
} else if (symbol != '[') {
segment.append(symbol);
}
}
if (literal.charAt(literal.length() - 1) != ']' && literal.charAt(literal.length() - 1) != '.') {
segment.append(literal.charAt(literal.length() - 1));
}
if (!segment.isEmpty() || literal.endsWith("[]")) {
segments.add(segment.toString());
}
if (segments.size() > 1 && segments.get(segments.size() - 1).equals("*") && !segments.get(segments.size() - 2).equals("*")) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "A single leading wildcard is not allowed. " +
"Use '" + literal.substring(0, literal.length() - 2) + "' instead.");
}
return segments;
}
private void validateSegment(String segment, String literal) {
if (isNumber(segment)) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "Numbers as key names are not supported.");
} else if (segment.startsWith("?")) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "Filter selectors are not supported.");
} else if (segment.contains(":")) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "Array slice selectors are not supported.");
} else if (segment.contains("(")) {
throw new IllegalArgumentException(ERROR_PREFIX.formatted(literal) + "Function extensions are not supported.");
}
}
private boolean isNumber(String segment) {
try {
Long.parseLong(segment);
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
/**
* Validates if the input set of JSONPath queries is ambiguous. Throws {@code java.lang.IllegalArgumentException#IllegalArgumentException} if it is.
* <p>
* The method does a lexical sort of input jsonpath queries, iterates over sorted values and checks if any local pair is ambiguous.
*
* @param jsonPaths input set of jsonpath queries
*/
public void checkAmbiguity(Set<JsonPath> jsonPaths) {
List<JsonPath> jsonPathList = jsonPaths.stream().sorted(Comparator.comparing(JsonPath::toString)).toList();
for (int i = 1; i < jsonPathList.size(); i++) {
JsonPath current = jsonPathList.get(i - 1);
JsonPath next = jsonPathList.get(i);
for (int j = 0; j < current.segments().length; j++) {
if (!current.segments()[j].equals(next.segments()[j])) {
if (current.segments()[j].equals("*") || next.segments()[j].equals("*")) {
throw new IllegalArgumentException(String.format("Ambiguous jsonpath keys. '%s' and '%s' combination is not supported.", current, next));
}
break;
}
if (j == current.segments().length - 1) { // covers cases like a ("$.a.b", "$.a.b.c") combination
throw new IllegalArgumentException(String.format("Ambiguous jsonpath keys. '%s' and '%s' combination is not supported.", current, next));
}
}
}
}
}