Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
QuerySpecBuilder.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 QuerySpecBuilder {
33
34 private static final int MIN_LIMIT = 1;
35 private static final int MAX_LIMIT = 200;
36 private static final int MIN_OFFSET = 0;
37 private static final int MAX_OFFSET = 100_000;
38
39 private final RsqlParser parser = new RsqlParser();
40
41 public QuerySpec fromRawParams(final String q, final String tags, final String locationId, final String enabled,
42 final String sort, final String limit, final String offset) {
43
44 final RsqlNode rsqlFilter = parser.parse(trimToNull(q));
45 final RsqlNode queryFilter = buildClassicFilter(tags, locationId, enabled);
46 final RsqlNode finalFilter = mergeWithAnd(rsqlFilter, queryFilter);
47
48 final int finalLimit = parseIntClamp(limit, QuerySpec.DEFAULT_LIMIT, MIN_LIMIT, MAX_LIMIT, "limit");
49 final int finalOffset = parseIntClamp(offset, QuerySpec.DEFAULT_OFFSET, MIN_OFFSET, MAX_OFFSET, "offset");
50 final var sorts = parseSort(sort);
51
52 return new QuerySpec(finalFilter, finalLimit, finalOffset, sorts);
53 }
54
55 private RsqlNode buildClassicFilter(final String tagsRaw, final String locationIdRaw, final String enabledRaw) {
56 final var filters = new ArrayList<RsqlNode>();
57
58 final var tags = parseCsv(tagsRaw);
59 if (!tags.isEmpty()) {
60 filters.add(new RsqlNode.Comp("tags", RsqlOperator.IN, tags));
61 }
62
63 final var locationId = trimToNull(locationIdRaw);
64 if (locationId != null) {
65 filters.add(new RsqlNode.Comp("locationId", RsqlOperator.EQ, List.of(locationId)));
66 }
67
68 final var enabled = trimToNull(enabledRaw);
69 if (enabled != null) {
70 final String normalized;
71 if ("true".equalsIgnoreCase(enabled) || "false".equalsIgnoreCase(enabled)) {
72 normalized = enabled.toLowerCase();
73 } else {
74 throw new IllegalArgumentException("enabled must be true or false");
75 }
76 filters.add(new RsqlNode.Comp("enabled", RsqlOperator.EQ, List.of(normalized)));
77 }
78
79 if (filters.isEmpty()) {
80 return null;
81 }
82 return filters.size() == 1 ? filters.get(0) : new RsqlNode.And(filters);
83 }
84
85 private List<Sort> parseSort(final String sortRaw) {
86 final var sort = trimToNull(sortRaw);
87 if (sort == null) {
88 return List.of();
89 }
90
91 final var out = new ArrayList<Sort>();
92 for (final String tokenRaw : sort.split(",")) {
93 final var token = tokenRaw.trim();
94 if (token.isEmpty()) {
95 continue;
96 }
97 if (token.startsWith("-")) {
98 if (token.length() == 1) {
99 throw new IllegalArgumentException("invalid sort field");
100 }
101 out.add(new Sort(token.substring(1), Sort.Direction.DESC));
102 } else {
103 out.add(new Sort(token, Sort.Direction.ASC));
104 }
105 }
106 return out;
107 }
108
109 private static List<String> parseCsv(final String raw) {
110 final var value = trimToNull(raw);
111 if (value == null) {
112 return List.of();
113 }
114 final var out = new ArrayList<String>();
115 for (final String tokenRaw : value.split(",")) {
116 final var token = tokenRaw.trim();
117 if (!token.isEmpty()) {
118 out.add(token);
119 }
120 }
121 return out;
122 }
123
124 private static int parseIntClamp(final String valueRaw, final int defaultValue, final int min, final int max,
125 final String fieldName) {
126 final var value = trimToNull(valueRaw);
127 if (value == null) {
128 return defaultValue;
129 }
130 try {
131 final var parsed = Integer.parseInt(value);
132 if (parsed < min) {
133 return min;
134 }
135 if (parsed > max) {
136 return max;
137 }
138 return parsed;
139 } catch (final NumberFormatException e) {
140 throw new IllegalArgumentException(fieldName + " must be an integer");
141 }
142 }
143
144 private static RsqlNode mergeWithAnd(final RsqlNode left, final RsqlNode right) {
145 if (left == null) {
146 return right;
147 }
148 if (right == null) {
149 return left;
150 }
151 return new RsqlNode.And(List.of(left, right));
152 }
153
154 private static String trimToNull(final String value) {
155 if (value == null) {
156 return null;
157 }
158 final var trimmed = value.trim();
159 return trimmed.isEmpty() ? null : trimmed;
160 }
161}
QuerySpec fromRawParams(final String q, final String tags, final String locationId, final String enabled, final String sort, final String limit, final String offset)
record Sort(String field, Direction direction)
Definition Sort.java:29
record QuerySpec(RsqlNode filter, int limit, int offset, List< Sort > sorts)