Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
RsqlParser.java
Go to the documentation of this file.
1package dev.rafex.ether.http.core.query;
2
3/*-
4 * #%L
5 * ether-http-core
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import java.util.ArrayList;
30import java.util.List;
31
32public final class RsqlParser {
33
34 public RsqlNode parse(final String input) {
35 if (input == null || input.isBlank()) {
36 return null;
37 }
38 final var state = new State(input);
39 final var node = parseOr(state);
40 state.skipWs();
41 if (!state.eof()) {
42 throw new IllegalArgumentException("invalid rsql near position " + state.pos);
43 }
44 return node;
45 }
46
47 private RsqlNode parseOr(final State state) {
48 final var nodes = new ArrayList<RsqlNode>();
49 nodes.add(parseAnd(state));
50 state.skipWs();
51 while (state.peek(',')) {
52 state.read();
53 nodes.add(parseAnd(state));
54 state.skipWs();
55 }
56 return nodes.size() == 1 ? nodes.get(0) : new RsqlNode.Or(nodes);
57 }
58
59 private RsqlNode parseAnd(final State state) {
60 final var nodes = new ArrayList<RsqlNode>();
61 nodes.add(parseTerm(state));
62 state.skipWs();
63 while (state.peek(';')) {
64 state.read();
65 nodes.add(parseTerm(state));
66 state.skipWs();
67 }
68 return nodes.size() == 1 ? nodes.get(0) : new RsqlNode.And(nodes);
69 }
70
71 private RsqlNode parseTerm(final State state) {
72 state.skipWs();
73 if (state.peek('(')) {
74 state.read();
75 final var node = parseOr(state);
76 state.skipWs();
77 state.expect(')');
78 return node;
79 }
80 return parseComparison(state);
81 }
82
83 private RsqlNode parseComparison(final State state) {
84 final var selector = parseSelector(state);
85 final var op = parseOperator(state);
86 final List<String> args;
87 if (op == RsqlOperator.IN || op == RsqlOperator.OUT) {
88 args = parseListArguments(state);
89 } else {
90 args = List.of(parseValue(state));
91 }
92 if (args.isEmpty()) {
93 throw new IllegalArgumentException("operator requires arguments");
94 }
95 return new RsqlNode.Comp(selector, op, args);
96 }
97
98 private String parseSelector(final State state) {
99 state.skipWs();
100 final int start = state.pos;
101 while (!state.eof()) {
102 final char c = state.ch();
103 if (Character.isLetterOrDigit(c) || c == '_' || c == '-') {
104 state.read();
105 continue;
106 }
107 break;
108 }
109 if (state.pos == start) {
110 throw new IllegalArgumentException("missing selector at position " + start);
111 }
112 return state.input.substring(start, state.pos);
113 }
114
115 private RsqlOperator parseOperator(final State state) {
116 state.skipWs();
117 if (state.match("==")) {
118 return RsqlOperator.EQ;
119 }
120 if (state.match("!=")) {
121 return RsqlOperator.NEQ;
122 }
123 if (state.match("=in=")) {
124 return RsqlOperator.IN;
125 }
126 if (state.match("=out=")) {
127 return RsqlOperator.OUT;
128 }
129 if (state.match("=like=")) {
130 return RsqlOperator.LIKE;
131 }
132 throw new IllegalArgumentException("unsupported operator at position " + state.pos);
133 }
134
135 private List<String> parseListArguments(final State state) {
136 state.skipWs();
137 state.expect('(');
138 final var args = new ArrayList<String>();
139 while (true) {
140 state.skipWs();
141 if (state.peek(')')) {
142 state.read();
143 break;
144 }
145 args.add(parseValue(state));
146 state.skipWs();
147 if (state.peek(',')) {
148 state.read();
149 continue;
150 }
151 state.expect(')');
152 break;
153 }
154 return args;
155 }
156
157 private String parseValue(final State state) {
158 state.skipWs();
159 if (state.peek('"')) {
160 return parseQuoted(state);
161 }
162 final int start = state.pos;
163 while (!state.eof()) {
164 final char c = state.ch();
165 if (c == ';' || c == ',' || c == ')') {
166 break;
167 }
168 if (Character.isWhitespace(c)) {
169 break;
170 }
171 state.read();
172 }
173 if (state.pos == start) {
174 throw new IllegalArgumentException("missing value at position " + start);
175 }
176 return state.input.substring(start, state.pos);
177 }
178
179 private String parseQuoted(final State state) {
180 state.expect('"');
181 final var out = new StringBuilder();
182 while (!state.eof()) {
183 final char c = state.read();
184 if (c == '\\') {
185 if (state.eof()) {
186 throw new IllegalArgumentException("unterminated escape sequence");
187 }
188 out.append(state.read());
189 continue;
190 }
191 if (c == '"') {
192 return out.toString();
193 }
194 out.append(c);
195 }
196 throw new IllegalArgumentException("unterminated quoted value");
197 }
198
199 private static final class State {
200 private final String input;
201 private int pos;
202
203 private State(final String input) {
204 this.input = input;
205 this.pos = 0;
206 }
207
208 private boolean eof() {
209 return pos >= input.length();
210 }
211
212 private char ch() {
213 return input.charAt(pos);
214 }
215
216 private char read() {
217 return input.charAt(pos++);
218 }
219
220 private boolean peek(final char c) {
221 return !eof() && ch() == c;
222 }
223
224 private void expect(final char c) {
225 if (!peek(c)) {
226 throw new IllegalArgumentException("expected '" + c + "' at position " + pos);
227 }
228 read();
229 }
230
231 private boolean match(final String s) {
232 if (input.regionMatches(pos, s, 0, s.length())) {
233 pos += s.length();
234 return true;
235 }
236 return false;
237 }
238
239 private void skipWs() {
240 while (!eof() && Character.isWhitespace(ch())) {
241 read();
242 }
243 }
244 }
245}
RsqlNode parse(final String input)