001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.beanquery; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import org.apache.juneau.commons.utils.StringUtils; 022 023import java.util.*; 024import java.util.function.*; 025import java.util.stream.*; 026 027/** 028 * A protocol-agnostic query request against a collection of beans: what to search, which columns to return, how to 029 * sort, and which page to return (design §5.1, §5.3). 030 * 031 * <p> 032 * Every field is a plain <b>raw value</b> — there are no HTTP-binding annotations, no {@link java.util.Optional}, 033 * no search map, no page string, and no parsed operators on this bean. A <jk>null</jk> field means <b>absent</b>. The 034 * grammar for {@link #getSearch() search} and {@link #getOpts() opts} lives <b>inside</b> those single strings and is 035 * decoded by a {@link BeanQuerySession session} when the query runs, not by this bean. 036 * 037 * <h5 class='section'>Fields</h5> 038 * <ul> 039 * <li><b>search</b> ({@link String}) — the one search string. Top-level commas (outside parentheses) separate 040 * clauses; the first unescaped {@code =} separates a column from its raw {@code $}-expression 041 * ({@link SearchExpressionParser}); backslash escapes {@code \,} / {@code \=} / {@code \\} are literals. Columns 042 * combine with {@code and}. See {@link ClauseParser}. 043 * <li><b>view</b> ({@link String}) — a comma-separated list of column names, in output order. 044 * <li><b>sort</b> ({@link String}) — a comma-separated list of {@code column} (ascending) or {@code column:desc} 045 * tokens (a trailing {@code -}/{@code +} is also accepted). 046 * <li><b>position</b> ({@link Integer}) — the 0-based first row of the page, or <jk>null</jk> for the start. 047 * <li><b>limit</b> ({@link Integer}) — the page size, or <jk>null</jk> for all rows. 048 * <li><b>opts</b> ({@link String}) — an open bag of {@code key=value} clauses (same grammar as {@code search}). 049 * The session reads {@code counts} and ignores unknown keys. 050 * </ul> 051 * 052 * <p> 053 * Construct with {@link #create()}, which returns a {@link Builder} that assembles these raw strings fluently: 054 * 055 * <h5 class='section'>Example:</h5> 056 * <p class='bjava'> 057 * BeanQuery <jv>q</jv> = BeanQuery.<jsm>create</jsm>() 058 * .eq(<js>"status"</js>, <js>"active"</js>) 059 * .or(<jv>g</jv> -> <jv>g</jv>.contains(<js>"name"</js>, <js>"bob"</js>).contains(<js>"email"</js>, <js>"bob"</js>)) 060 * .sort(<js>"name"</js>) 061 * .limit(50) 062 * .build(); 063 * </p> 064 * 065 * <p> 066 * The HTTP-binding {@code BeanQueryRequest} ({@code juneau-rest-common}) extends this class and adds the 067 * {@code @Query} bindings. 068 * 069 * @since 10.0.0 070 */ 071@SuppressWarnings({ 072 "java:S1192" // Duplicated argument-name literals read more clearly inline than as constants. 073}) 074public class BeanQuery { 075 076 private String search; 077 private String view; 078 private String sort; 079 private Integer position; 080 private Integer limit; 081 private String opts; 082 083 /** 084 * Creates a new {@link Builder}. 085 * 086 * @return A new builder. 087 */ 088 public static Builder create() { 089 return new Builder(); 090 } 091 092 /** 093 * The one search string. 094 * 095 * @return The search string, or <jk>null</jk> if absent. 096 */ 097 public String getSearch() { 098 return search; 099 } 100 101 /** 102 * Sets the one search string. 103 * 104 * @param value The search string. Can be <jk>null</jk> to unset. 105 * @return This object. 106 */ 107 public BeanQuery setSearch(String value) { 108 search = value; 109 return this; 110 } 111 112 /** 113 * The raw view (column-selection) directive. 114 * 115 * @return The view string, or <jk>null</jk> if absent. 116 */ 117 public String getView() { 118 return view; 119 } 120 121 /** 122 * Sets the raw view (column-selection) directive. 123 * 124 * @param value The view string. Can be <jk>null</jk> to unset. 125 * @return This object. 126 */ 127 public BeanQuery setView(String value) { 128 view = value; 129 return this; 130 } 131 132 /** 133 * The raw sort directive. 134 * 135 * @return The sort string, or <jk>null</jk> if absent. 136 */ 137 public String getSort() { 138 return sort; 139 } 140 141 /** 142 * Sets the raw sort directive. 143 * 144 * @param value The sort string. Can be <jk>null</jk> to unset. 145 * @return This object. 146 */ 147 public BeanQuery setSort(String value) { 148 sort = value; 149 return this; 150 } 151 152 /** 153 * The 0-based first row of the page. 154 * 155 * @return The position, or <jk>null</jk> for the start of the results. 156 */ 157 public Integer getPosition() { 158 return position; 159 } 160 161 /** 162 * Sets the 0-based first row of the page. 163 * 164 * @param value The position. Can be <jk>null</jk> to unset. 165 * @return This object. 166 */ 167 public BeanQuery setPosition(Integer value) { 168 position = value; 169 return this; 170 } 171 172 /** 173 * The page size. 174 * 175 * @return The limit, or <jk>null</jk> for all rows. 176 */ 177 public Integer getLimit() { 178 return limit; 179 } 180 181 /** 182 * Sets the page size. 183 * 184 * @param value The limit. Can be <jk>null</jk> to unset (all rows). 185 * @return This object. 186 */ 187 public BeanQuery setLimit(Integer value) { 188 limit = value; 189 return this; 190 } 191 192 /** 193 * The one opts string (an open bag of {@code key=value} clauses). 194 * 195 * @return The opts string, or <jk>null</jk> if absent. 196 */ 197 public String getOpts() { 198 return opts; 199 } 200 201 /** 202 * Sets the one opts string. 203 * 204 * @param value The opts string. Can be <jk>null</jk> to unset. 205 * @return This object. 206 */ 207 public BeanQuery setOpts(String value) { 208 opts = value; 209 return this; 210 } 211 212 /** 213 * Creates a {@link Builder} pre-populated with this query's current state, so individual settings can be 214 * changed or added without disturbing the rest. {@link #getSearch()} is re-parsed into the builder's item 215 * tree, so a new condition on an existing top-level column merges through <c>$and(...)</c> rather than being 216 * dropped or duplicated. 217 * 218 * @return A new builder seeded from this query. 219 */ 220 public Builder copy() { 221 var b = new Builder(); 222 if (search != null) 223 b.group.items.seed(SearchParser.parse(search)); 224 if (view != null) 225 b.view.addAll(splitNonBlank(view)); 226 if (sort != null) 227 b.sort.addAll(splitNonBlank(sort)); 228 b.position = position; 229 b.limit = limit; 230 if (opts != null) 231 b.opts.putAll(ClauseParser.parse(opts)); 232 return b; 233 } 234 235 /** Splits a comma-delimited list, trimming each token and dropping blank ones. */ 236 private static List<String> splitNonBlank(String s) { 237 var l = new ArrayList<>(StringUtils.split(s)); 238 l.removeIf(String::isEmpty); 239 return l; 240 } 241 242 @Override 243 public boolean equals(Object o) { 244 if (this == o) 245 return true; 246 if (!(o instanceof BeanQuery other)) 247 return false; 248 return eq(search, other.search) && eq(view, other.view) 249 && eq(sort, other.sort) && eq(position, other.position) 250 && eq(limit, other.limit) && eq(opts, other.opts); 251 } 252 253 @Override 254 public int hashCode() { 255 return h(search, view, sort, position, limit, opts); 256 } 257 258 @Override 259 public String toString() { 260 return "BeanQuery[search=" + search + ", view=" + view + ", sort=" + sort + ", position=" + position 261 + ", limit=" + limit + ", opts=" + opts + "]"; 262 } 263 264 /** 265 * A group of cross-column conditions built with {@link BeanQuery.Builder#or(Consumer)}, 266 * {@link BeanQuery.Builder#and(Consumer)}, or {@link BeanQuery.Builder#not(Consumer)}. 267 * 268 * <p>Exposes the same condition methods as {@link Builder}, minus paging/view/sort/opts, since a group only 269 * ever contributes conditions to its enclosing level. 270 * 271 * <h5 class='section'>Example:</h5> 272 * <p class='bjava'> 273 * BeanQuery.<jsm>create</jsm>().or(<jv>g</jv> -> <jv>g</jv>.eq(<js>"a"</js>, 1).eq(<js>"b"</js>, 2)); 274 * <jc>// -> "$or(a=$eq(\"1\"),b=$eq(\"2\"))"</jc> 275 * </p> 276 * 277 * @since 10.0.0 278 */ 279 public static final class GroupBuilder { 280 281 final SearchItemBuilder items = new SearchItemBuilder(); 282 283 GroupBuilder() {} 284 285 /** 286 * Adds an equals condition. 287 * 288 * @param column The column name. 289 * @param value The value to compare against. 290 * @return This object. 291 */ 292 public GroupBuilder eq(String column, Object value) { 293 return emit(column, "$eq", value); 294 } 295 296 /** 297 * Adds a not-equals condition, true if the column matches none of {@code values}. 298 * 299 * @param column The column name. 300 * @param values The values to compare against. 301 * @return This object. 302 */ 303 public GroupBuilder ne(String column, Object...values) { 304 return emit(column, "$ne", values); 305 } 306 307 /** 308 * Adds an in-list condition, true if the column matches any of {@code values}. 309 * 310 * @param column The column name. 311 * @param values The values to compare against. 312 * @return This object. 313 */ 314 public GroupBuilder in(String column, Object...values) { 315 return emit(column, "$in", values); 316 } 317 318 /** 319 * Adds a substring-match condition. 320 * 321 * @param column The column name. 322 * @param value The substring to search for. 323 * @return This object. 324 */ 325 public GroupBuilder contains(String column, Object value) { 326 return emit(column, "$contains", value); 327 } 328 329 /** 330 * Adds a prefix-match condition. 331 * 332 * @param column The column name. 333 * @param value The prefix to match. 334 * @return This object. 335 */ 336 public GroupBuilder prefix(String column, Object value) { 337 return emit(column, "$prefix", value); 338 } 339 340 /** 341 * Adds a blank-value condition. 342 * 343 * @param column The column name. 344 * @return This object. 345 */ 346 public GroupBuilder blank(String column) { 347 reqnb("column", column); 348 items.addLeaf(column, "$blank()"); 349 return this; 350 } 351 352 /** 353 * Adds a not-blank condition. 354 * 355 * @param column The column name. 356 * @return This object. 357 */ 358 public GroupBuilder notBlank(String column) { 359 reqnb("column", column); 360 items.addLeaf(column, "$not($blank())"); 361 return this; 362 } 363 364 /** 365 * Adds a greater-than condition. 366 * 367 * @param column The column name. 368 * @param value The value to compare against. 369 * @return This object. 370 */ 371 public GroupBuilder gt(String column, Object value) { 372 return emit(column, "$gt", value); 373 } 374 375 /** 376 * Adds a greater-than-or-equal condition. 377 * 378 * @param column The column name. 379 * @param value The value to compare against. 380 * @return This object. 381 */ 382 public GroupBuilder gte(String column, Object value) { 383 return emit(column, "$gte", value); 384 } 385 386 /** 387 * Adds a less-than condition. 388 * 389 * @param column The column name. 390 * @param value The value to compare against. 391 * @return This object. 392 */ 393 public GroupBuilder lt(String column, Object value) { 394 return emit(column, "$lt", value); 395 } 396 397 /** 398 * Adds a less-than-or-equal condition. 399 * 400 * @param column The column name. 401 * @param value The value to compare against. 402 * @return This object. 403 */ 404 public GroupBuilder lte(String column, Object value) { 405 return emit(column, "$lte", value); 406 } 407 408 /** 409 * Adds a between condition, inclusive of both bounds. 410 * 411 * @param column The column name. 412 * @param lower The lower bound. 413 * @param upper The upper bound. 414 * @return This object. 415 */ 416 public GroupBuilder between(String column, Object lower, Object upper) { 417 return emit(column, "$between", lower, upper); 418 } 419 420 /** 421 * Adds a regex-match condition. The session still rejects this unless regex search is enabled 422 * (context/session design D7). 423 * 424 * @param column The column name. 425 * @param pattern The regular expression. 426 * @return This object. 427 */ 428 public GroupBuilder regex(String column, String pattern) { 429 return emit(column, "$regex", pattern); 430 } 431 432 /** 433 * Adds a regex-match condition with flags. The session still rejects this unless regex search is enabled 434 * (context/session design D7). 435 * 436 * @param column The column name. 437 * @param pattern The regular expression. 438 * @param flags The regex flags (engine-specific, e.g. {@code "i"} for case-insensitive). 439 * @return This object. 440 */ 441 public GroupBuilder regex(String column, String pattern, String flags) { 442 return emit(column, "$regex", pattern, flags); 443 } 444 445 /** 446 * Escape hatch for an operator not exposed by a dedicated method above. {@code name} must start with 447 * {@code "$"}. 448 * 449 * @param column The column name. 450 * @param name The operator name, starting with {@code "$"}. 451 * @param values The operator's arguments. 452 * @return This object. 453 */ 454 public GroupBuilder op(String column, String name, Object...values) { 455 req(name != null && name.startsWith("$"), 456 "BeanQuery.GroupBuilder.op requires an operator name starting with '$': '%s'.", name); 457 return emit(column, name, values); 458 } 459 460 /** 461 * Escape hatch for a raw, already-formatted expression. Unlike every other condition method, the 462 * expression is used as-is and is not quoted. 463 * 464 * @param column The column name. 465 * @param expression The raw expression, e.g. {@code "$eq(\"bob\")"}. 466 * @return This object. 467 */ 468 public GroupBuilder search(String column, String expression) { 469 reqnb("column", column); 470 reqnn("expression", expression); 471 items.addLeaf(column, expression); 472 return this; 473 } 474 475 /** 476 * Adds a cross-column <c>$or(...)</c> group. A block that adds no items adds nothing; a block that adds 477 * exactly one item emits that item unwrapped (no <c>$or(...)</c> wrapper). 478 * 479 * @param block Populates the group's conditions on a fresh {@link GroupBuilder}. 480 * @return This object. 481 */ 482 public GroupBuilder or(Consumer<GroupBuilder> block) { 483 items.addGroup(group(block, SearchItem.Or::new)); 484 return this; 485 } 486 487 /** 488 * Adds a cross-column <c>$and(...)</c> group. A block that adds no items adds nothing; a block that adds 489 * exactly one item emits that item unwrapped (no <c>$and(...)</c> wrapper). 490 * 491 * @param block Populates the group's conditions on a fresh {@link GroupBuilder}. 492 * @return This object. 493 */ 494 public GroupBuilder and(Consumer<GroupBuilder> block) { 495 items.addGroup(group(block, SearchItem.And::new)); 496 return this; 497 } 498 499 /** 500 * Adds a <c>$not(...)</c> group. The block must add exactly one item (a leaf or a nested group); any 501 * other count throws {@link IllegalArgumentException}. 502 * 503 * @param block Populates the single negated condition on a fresh {@link GroupBuilder}. 504 * @return This object. 505 * @throws IllegalArgumentException If the block adds a number of items other than exactly one. 506 */ 507 public GroupBuilder not(Consumer<GroupBuilder> block) { 508 reqnn("block", block); 509 var inner = new GroupBuilder(); 510 block.accept(inner); 511 var innerItems = inner.items.items(); 512 req(innerItems.size() == 1, 513 "BeanQuery.GroupBuilder.not(...) requires exactly one condition, found %s.", innerItems.size()); 514 items.addGroup(new SearchItem.Not(innerItems.get(0))); 515 return this; 516 } 517 518 private static SearchItem group(Consumer<GroupBuilder> block, Function<List<SearchItem>,SearchItem> wrap) { 519 reqnn("block", block); 520 var inner = new GroupBuilder(); 521 block.accept(inner); 522 var innerItems = inner.items.items(); 523 if (innerItems.isEmpty()) 524 return null; 525 if (innerItems.size() == 1) 526 return innerItems.get(0); 527 return wrap.apply(innerItems); 528 } 529 530 private GroupBuilder emit(String column, String name, Object...values) { 531 reqnb("column", column); 532 req(values != null && values.length > 0, 533 "Operator '%s' on column '%s' requires at least one value.", name, column); 534 var body = Arrays.stream(values).map(GroupBuilder::quote).collect(Collectors.joining(",")); 535 items.addLeaf(column, name + "(" + body + ")"); 536 return this; 537 } 538 539 private static String quote(Object value) { 540 var s = ValueFormat.format(value); 541 // Escape a literal '$' by doubling it, in addition to (not instead of) the quote-escaping below: 542 // decodeLiteral unconditionally collapses $$ -> $ as its last decode step, so an un-escaped 543 // literal "$$" here would wrongly round-trip to "$". (The two replace() calls operate on 544 // disjoint alphabets, so the order between them doesn't matter - only that both happen.) 545 return "\"" + s.replace("$", "$$").replace("\"", "\"\"") + "\""; 546 } 547 } 548 549 /** 550 * Builds an immutable {@link BeanQuery}. Create with {@link BeanQuery#create()}; derive one from an existing 551 * query with {@link BeanQuery#copy()}. 552 * 553 * <p>Exposes every {@link GroupBuilder} condition method (they add top-level conditions here) plus 554 * view/sort/paging/opts/counts and {@link #build()}. 555 * 556 * <h5 class='section'>Example:</h5> 557 * <p class='bjava'> 558 * BeanQuery <jv>q</jv> = BeanQuery.<jsm>create</jsm>().eq(<js>"status"</js>, <js>"active"</js>).build(); 559 * </p> 560 * 561 * @since 10.0.0 562 */ 563 public static final class Builder { 564 565 private final GroupBuilder group = new GroupBuilder(); 566 private final List<String> view = new ArrayList<>(); 567 private final List<String> sort = new ArrayList<>(); 568 private final Map<String,String> opts = new LinkedHashMap<>(); 569 private Integer position; 570 private Integer limit; 571 572 Builder() {} 573 574 /** Same as {@link GroupBuilder#eq(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 575 public Builder eq(String column, Object value) { group.eq(column, value); return this; } 576 577 /** Same as {@link GroupBuilder#ne(String,Object...)}, adding a top-level condition. @param column The column name. @param values The values. @return This object. */ 578 public Builder ne(String column, Object...values) { group.ne(column, values); return this; } 579 580 /** Same as {@link GroupBuilder#in(String,Object...)}, adding a top-level condition. @param column The column name. @param values The values. @return This object. */ 581 public Builder in(String column, Object...values) { group.in(column, values); return this; } 582 583 /** Same as {@link GroupBuilder#contains(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 584 public Builder contains(String column, Object value) { group.contains(column, value); return this; } 585 586 /** Same as {@link GroupBuilder#prefix(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 587 public Builder prefix(String column, Object value) { group.prefix(column, value); return this; } 588 589 /** Same as {@link GroupBuilder#blank(String)}, adding a top-level condition. @param column The column name. @return This object. */ 590 public Builder blank(String column) { group.blank(column); return this; } 591 592 /** Same as {@link GroupBuilder#notBlank(String)}, adding a top-level condition. @param column The column name. @return This object. */ 593 public Builder notBlank(String column) { group.notBlank(column); return this; } 594 595 /** Same as {@link GroupBuilder#gt(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 596 public Builder gt(String column, Object value) { group.gt(column, value); return this; } 597 598 /** Same as {@link GroupBuilder#gte(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 599 public Builder gte(String column, Object value) { group.gte(column, value); return this; } 600 601 /** Same as {@link GroupBuilder#lt(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 602 public Builder lt(String column, Object value) { group.lt(column, value); return this; } 603 604 /** Same as {@link GroupBuilder#lte(String,Object)}, adding a top-level condition. @param column The column name. @param value The value. @return This object. */ 605 public Builder lte(String column, Object value) { group.lte(column, value); return this; } 606 607 /** Same as {@link GroupBuilder#between(String,Object,Object)}, adding a top-level condition. @param column The column name. @param lower The lower bound. @param upper The upper bound. @return This object. */ 608 public Builder between(String column, Object lower, Object upper) { group.between(column, lower, upper); return this; } 609 610 /** Same as {@link GroupBuilder#regex(String,String)}, adding a top-level condition. @param column The column name. @param pattern The regular expression. @return This object. */ 611 public Builder regex(String column, String pattern) { group.regex(column, pattern); return this; } 612 613 /** Same as {@link GroupBuilder#regex(String,String,String)}, adding a top-level condition. @param column The column name. @param pattern The regular expression. @param flags The regex flags. @return This object. */ 614 public Builder regex(String column, String pattern, String flags) { group.regex(column, pattern, flags); return this; } 615 616 /** Same as {@link GroupBuilder#op(String,String,Object...)}, adding a top-level condition. @param column The column name. @param name The operator name, starting with {@code "$"}. @param values The operator's arguments. @return This object. */ 617 public Builder op(String column, String name, Object...values) { group.op(column, name, values); return this; } 618 619 /** Same as {@link GroupBuilder#search(String,String)}, adding a top-level condition. @param column The column name. @param expression The raw expression. @return This object. */ 620 public Builder search(String column, String expression) { group.search(column, expression); return this; } 621 622 /** Same as {@link GroupBuilder#or(Consumer)}, adding a top-level group. @param block Populates the group. @return This object. */ 623 public Builder or(Consumer<GroupBuilder> block) { group.or(block); return this; } 624 625 /** Same as {@link GroupBuilder#and(Consumer)}, adding a top-level group. @param block Populates the group. @return This object. */ 626 public Builder and(Consumer<GroupBuilder> block) { group.and(block); return this; } 627 628 /** Same as {@link GroupBuilder#not(Consumer)}, adding a top-level group. @param block Populates the negated condition. @return This object. @throws IllegalArgumentException If the block adds a number of items other than exactly one. */ 629 public Builder not(Consumer<GroupBuilder> block) { group.not(block); return this; } 630 631 /** 632 * Appends columns to the view list; does not replace columns already added. 633 * 634 * @param columns The columns to append. Must not be <jk>null</jk>, nor contain a <jk>null</jk> element. 635 * @return This object. 636 * @throws IllegalArgumentException If {@code columns} is <jk>null</jk> or contains a <jk>null</jk> element. 637 */ 638 public Builder view(String...columns) { 639 reqnns("columns", columns); 640 Collections.addAll(view, columns); 641 return this; 642 } 643 644 /** 645 * Appends an ascending sort key; does not replace keys already added. 646 * 647 * @param column The column to sort by. 648 * @return This object. 649 */ 650 public Builder sort(String column) { 651 reqnb("column", column); 652 sort.add(column); 653 return this; 654 } 655 656 /** 657 * Appends a descending sort key ({@code column + ":desc"}); does not replace keys already added. 658 * 659 * @param column The column to sort by, descending. 660 * @return This object. 661 */ 662 public Builder sortDesc(String column) { 663 reqnb("column", column); 664 sort.add(column + ":desc"); 665 return this; 666 } 667 668 /** 669 * Sets the zero-based row offset. 670 * 671 * @param position The row offset; must not be negative. 672 * @return This object. 673 * @throws IllegalArgumentException If {@code position} is negative. 674 */ 675 public Builder position(int position) { 676 req(position >= 0, "BeanQuery.Builder.position must not be negative: %s.", position); 677 this.position = position; 678 return this; 679 } 680 681 /** 682 * Sets the maximum number of rows to return. A negative value is clamped to the literal {@code -1} 683 * (meaning "as many rows as the session allows"), never dropped to {@code null}. 684 * 685 * @param limit The row limit. 686 * @return This object. 687 */ 688 public Builder limit(int limit) { 689 this.limit = limit < 0 ? -1 : limit; 690 return this; 691 } 692 693 /** 694 * Sets {@link #position(int)} and {@link #limit(int)} together. 695 * 696 * @param start The zero-based row offset; must not be negative. 697 * @param length The maximum number of rows to return; a negative value means "as many as allowed". 698 * @return This object. 699 * @throws IllegalArgumentException If {@code start} is negative. 700 */ 701 public Builder page(int start, int length) { 702 position(start); 703 return limit(length); 704 } 705 706 /** 707 * Sets a free-form option, overwriting any earlier value for the same key. 708 * 709 * @param key The option name. Must not be <jk>null</jk> or blank. 710 * @param value The option value. Must not be <jk>null</jk>. 711 * @return This object. 712 * @throws IllegalArgumentException If {@code key} is <jk>null</jk> or blank, or {@code value} is <jk>null</jk>. 713 */ 714 public Builder opt(String key, String value) { 715 reqnb("key", key); 716 reqnn("value", value); 717 opts.put(key, value); 718 return this; 719 } 720 721 /** 722 * Sets the {@code counts} option from a {@link CountRequest}. {@link CountRequest#NONE} removes the 723 * {@code counts} option entirely (rather than writing a {@code "none"} value). 724 * 725 * @param request The requested counts. 726 * @return This object. 727 */ 728 public Builder counts(CountRequest request) { 729 reqnn("request", request); 730 if (request == CountRequest.NONE) { 731 opts.remove("counts"); 732 return this; 733 } 734 return opt("counts", request.name().toLowerCase(Locale.ROOT)); 735 } 736 737 /** 738 * Builds a new, immutable {@link BeanQuery} from this builder's current state. The builder remains usable 739 * afterward; later calls do not affect the {@link BeanQuery} already built. 740 * 741 * @return A new {@link BeanQuery}. 742 */ 743 public BeanQuery build() { 744 var q = new BeanQuery(); 745 var items = group.items.items(); 746 if (!items.isEmpty()) 747 q.setSearch(SearchParser.render(items)); 748 if (!view.isEmpty()) 749 q.setView(String.join(",", view)); 750 if (!sort.isEmpty()) 751 q.setSort(String.join(",", sort)); 752 q.setPosition(position); 753 q.setLimit(limit); 754 if (!opts.isEmpty()) 755 q.setOpts(opts.entrySet().stream() 756 .map(e -> ClauseParser.escape(e.getKey()) + "=" + ClauseParser.escape(e.getValue())) 757 .collect(Collectors.joining(","))); 758 return q; 759 } 760 } 761}