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;
42 final String sort,
final String limit,
final String offset) {
44 final RsqlNode rsqlFilter = parser.parse(trimToNull(q));
45 final RsqlNode queryFilter = buildClassicFilter(tags, locationId, enabled);
46 final RsqlNode finalFilter = mergeWithAnd(rsqlFilter, queryFilter);
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);
52 return new QuerySpec(finalFilter, finalLimit, finalOffset, sorts);
55 private RsqlNode buildClassicFilter(
final String tagsRaw,
final String locationIdRaw,
final String enabledRaw) {
56 final var filters =
new ArrayList<RsqlNode>();
58 final var tags = parseCsv(tagsRaw);
59 if (!tags.isEmpty()) {
63 final var locationId = trimToNull(locationIdRaw);
64 if (locationId !=
null) {
65 filters.add(
new RsqlNode.Comp(
"locationId", RsqlOperator.EQ, List.of(locationId)));
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();
74 throw new IllegalArgumentException(
"enabled must be true or false");
76 filters.add(
new RsqlNode.Comp(
"enabled", RsqlOperator.EQ, List.of(normalized)));
79 if (filters.isEmpty()) {
82 return filters.size() == 1 ? filters.get(0) :
new RsqlNode.And(filters);
85 private List<Sort> parseSort(
final String sortRaw) {
86 final var sort = trimToNull(sortRaw);
91 final var out =
new ArrayList<Sort>();
92 for (
final String tokenRaw : sort.split(
",")) {
93 final var token = tokenRaw.trim();
94 if (token.isEmpty()) {
97 if (token.startsWith(
"-")) {
98 if (token.length() == 1) {
99 throw new IllegalArgumentException(
"invalid sort field");
101 out.add(
new Sort(token.substring(1),
Sort.Direction.DESC));
103 out.add(
new Sort(token,
Sort.Direction.ASC));
109 private static List<String> parseCsv(
final String raw) {
110 final var value = trimToNull(raw);
114 final var out =
new ArrayList<String>();
115 for (
final String tokenRaw : value.split(
",")) {
116 final var token = tokenRaw.trim();
117 if (!token.isEmpty()) {
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);
131 final var parsed = Integer.parseInt(value);
139 }
catch (
final NumberFormatException e) {
140 throw new IllegalArgumentException(fieldName +
" must be an integer");
144 private static RsqlNode mergeWithAnd(
final RsqlNode left,
final RsqlNode right) {
151 return new RsqlNode.And(List.of(left, right));
154 private static String trimToNull(
final String value) {
158 final var trimmed = value.trim();
159 return trimmed.isEmpty() ? null : trimmed;