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 java.time.*; 022import java.util.*; 023 024/** 025 * The reusable, immutable, server-side half of a BeanQuery engine: which columns may be searched, sorted and viewed, 026 * which operators apply, and the limits every session enforces. 027 * 028 * <p> 029 * Build a context once, at startup, from its engine's {@code create(...)} factory. It holds no rows and no 030 * connections, so it can be shared across threads. Open a {@link BeanQuerySession} per request with the engine's 031 * {@code getSession(...)} shortcut or with {@link #createSession()}. Call {@link #copy()} to derive a variant. 032 * 033 * <p> 034 * The base class owns every engine-independent check (spec ยง5): the column allow-list, operator/type checks, the 035 * regex gate, the size caps, limit clamping and the count policy. Engines see only a {@link ResolvedQuery}. 036 * 037 * <h5 class='section'>Example:</h5> 038 * <p class='bjava'> 039 * <jc>// Once, at startup.</jc> 040 * InMemoryBeanQueryContext<Person> <jv>people</jv> = InMemoryBeanQueryContext 041 * .<jsm>create</jsm>(Person.<jk>class</jk>) <jc>// Columns: every getter on Person.</jc> 042 * .exclude(<js>"passwordHash"</js>) <jc>// Never searchable, sortable or visible.</jc> 043 * .column(<js>"version"</js>, SearchType.<jsf>VERSION</jsf>) 044 * .maxLimit(500) 045 * .build(); 046 * 047 * <jc>// A stricter variant for a public page.</jc> 048 * InMemoryBeanQueryContext<Person> <jv>guest</jv> = <jv>people</jv>.copy().allowRegex(<jk>false</jk>).defaultLimit(50).maxLimit(50).build(); 049 * 050 * <jc>// Per request.</jc> 051 * <jk>try</jk> (InMemoryBeanQuerySession<Person> <jv>session</jv> = <jv>people</jv>.getSession(<jv>rows</jv>)) { 052 * Page<Person> <jv>page</jv> = <jv>session</jv>.find(<jv>query</jv>); 053 * } 054 * </p> 055 * 056 * @param <T> The row (bean) type. 057 * @since 10.0.0 058 */ 059@SuppressWarnings({ 060 "java:S1192", // Duplicated argument-name literals read more clearly inline than as constants. 061 "java:S1452" // Public self-typed builder API; the concrete SELF is unknowable to callers. 062}) 063public abstract class BeanQueryContext<T> { 064 065 /** 066 * The settings every engine's context builder shares. 067 * 068 * <p> 069 * Single-argument problems throw {@link IllegalArgumentException} from the setter. Missing or conflicting settings 070 * throw {@link IllegalStateException} from {@link #build()}. 071 * 072 * @param <T> The row type. 073 * @param <SELF> The concrete builder type, returned by every setter. 074 */ 075 @SuppressWarnings({ 076 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 077 }) 078 public abstract static class Builder<T,SELF extends Builder<T,SELF>> { 079 080 final QuerySettings settings; 081 private boolean defaultLimitExplicit; // True once defaultLimit(int) is called on THIS builder. 082 083 /** Constructor for a new, empty builder. */ 084 protected Builder() { 085 settings = new QuerySettings(); 086 } 087 088 /** 089 * Copy constructor. 090 * 091 * @param copyFrom The context whose settings are copied. Must not be <jk>null</jk>. 092 */ 093 protected Builder(BeanQueryContext<T> copyFrom) { 094 settings = reqnn("copyFrom", copyFrom).settings.copy(); 095 } 096 097 /** 098 * Returns this builder as its concrete type. 099 * 100 * @return This object. 101 */ 102 @SuppressWarnings({ 103 "unchecked" // CRTP self-type cast is safe by construction. 104 }) 105 protected final SELF self() { 106 return (SELF)this; 107 } 108 109 /** 110 * Declares a column, or changes the type of a declared one (keeping its position). 111 * 112 * @param name The column name. Must not be blank. 113 * @param type The column's value type. Must not be <jk>null</jk>. 114 * @return This object. 115 */ 116 public SELF column(String name, SearchType type) { 117 reqnb("name", name); 118 settings.columns.put(name, reqnn("type", type)); 119 return self(); 120 } 121 122 /** 123 * Keeps only the specified declared columns, in the specified order. 124 * 125 * @param names The column names. Each must already be declared. 126 * @return This object. 127 */ 128 public SELF columns(String...names) { 129 var m = new LinkedHashMap<String,SearchType>(); 130 for (var n : reqnns("names", names)) 131 m.put(n, settings.columns.get(declared(n))); 132 settings.columns.clear(); 133 settings.columns.putAll(m); 134 settings.columnOperators.keySet().retainAll(m.keySet()); 135 return self(); 136 } 137 138 /** 139 * Removes declared columns (and any operator override on them). 140 * 141 * @param names The column names. Each must be declared. 142 * @return This object. 143 */ 144 public SELF exclude(String...names) { 145 for (var n : reqnns("names", names)) { 146 settings.columns.remove(declared(n)); 147 settings.columnOperators.remove(n); 148 } 149 return self(); 150 } 151 152 /** 153 * Checks that a column is declared. 154 * 155 * @param name The column name. 156 * @return The same name. 157 * @throws IllegalArgumentException If the column is not declared. 158 */ 159 protected final String declared(String name) { 160 if (! settings.columns.containsKey(name)) 161 throw iaex("Column '%s' is not declared.", name); 162 return name; 163 } 164 165 /** 166 * Sets the default operator set (for columns without an override). Default: {@link SearchOperatorSet#standard()}. 167 * 168 * @param value The operator set. Must not be <jk>null</jk>. 169 * @return This object. 170 */ 171 public SELF operators(SearchOperatorSet value) { 172 settings.defaultOperators = reqnn("value", value); 173 return self(); 174 } 175 176 /** 177 * Replaces the operator set for one declared column. 178 * 179 * @param column The column name. Must be declared. 180 * @param value The operator set. Must not be <jk>null</jk>. 181 * @return This object. 182 */ 183 public SELF columnOperators(String column, SearchOperatorSet value) { 184 settings.columnOperators.put(declared(column), reqnn("value", value)); 185 return self(); 186 } 187 188 /** 189 * Allows the {@code $regex} operator. Default: <jk>true</jk>; {@code allowRegex(false)} disables it. 190 * 191 * @param value The new value. 192 * @return This object. 193 */ 194 public SELF allowRegex(boolean value) { 195 settings.allowRegex = value; 196 return self(); 197 } 198 199 /** 200 * Sets the time budget for all {@code $regex} matching in one session call. Default: 50 ms. 201 * 202 * @param value The budget. Must be positive. 203 * @return This object. 204 */ 205 public SELF regexTimeout(Duration value) { 206 settings.regexTimeout = QuerySettings.positive("regexTimeout", value); 207 return self(); 208 } 209 210 /** 211 * Sets which counts {@code find} / {@code findValues} compute. Default: {@link CountPolicy#IF_REQUESTED}. 212 * 213 * @param value The policy. Must not be <jk>null</jk>. 214 * @return This object. 215 */ 216 public SELF countPolicy(CountPolicy value) { 217 settings.countPolicy = reqnn("value", value); 218 return self(); 219 } 220 221 /** 222 * Sets the limit used when a query has none. Default: 100. 223 * 224 * @param value The limit. Must be positive. 225 * @return This object. 226 */ 227 public SELF defaultLimit(int value) { 228 settings.defaultLimit = QuerySettings.positive("defaultLimit", value); 229 defaultLimitExplicit = true; 230 return self(); 231 } 232 233 /** 234 * Sets the largest limit a query can ask for; larger limits and {@code limit=-1} are clamped to it. 235 * Default: 1000. 236 * 237 * <p> 238 * If this builder's {@code defaultLimit} was never explicitly set (it is still {@code QuerySettings}'s own 239 * baseline default, or โ for a builder from {@link #copy()} โ still whatever the source context had), 240 * narrowing {@code maxLimit} below it auto-narrows {@code defaultLimit} to match, rather than leaving the two 241 * in conflict. An explicitly-chosen {@code defaultLimit} is never silently changed by this setter โ 242 * a resulting conflict is left for {@link #build()} to reject. 243 * 244 * @param value The cap, or <jk>null</jk> for no cap. Must be positive if set. 245 * @return This object. 246 */ 247 public SELF maxLimit(Integer value) { 248 settings.maxLimit = QuerySettings.positiveOrNull("maxLimit", value); 249 if (! defaultLimitExplicit && settings.maxLimit != null && settings.defaultLimit > settings.maxLimit) 250 settings.defaultLimit = settings.maxLimit; 251 return self(); 252 } 253 254 /** 255 * Sets the largest number of search clauses. Default: 64. 256 * 257 * @param value The cap. Must be positive. 258 * @return This object. 259 */ 260 public SELF maxSearchClauses(int value) { 261 settings.maxSearchClauses = QuerySettings.positive("maxSearchClauses", value); 262 return self(); 263 } 264 265 /** 266 * Sets the largest number of sort keys. Default: 8. 267 * 268 * @param value The cap. Must be positive. 269 * @return This object. 270 */ 271 public SELF maxSortKeys(int value) { 272 settings.maxSortKeys = QuerySettings.positive("maxSortKeys", value); 273 return self(); 274 } 275 276 /** 277 * Sets the longest search, sort, view or opts string. Default: 4096. 278 * 279 * @param value The cap. Must be positive. 280 * @return This object. 281 */ 282 public SELF maxSearchLength(int value) { 283 settings.maxSearchLength = QuerySettings.positive("maxSearchLength", value); 284 return self(); 285 } 286 287 /** 288 * Sets the deepest allowed expression nesting. Default: 16. 289 * 290 * @param value The cap. Must be positive. 291 * @return This object. 292 */ 293 public SELF maxExpressionDepth(int value) { 294 settings.maxExpressionDepth = QuerySettings.positive("maxExpressionDepth", value); 295 return self(); 296 } 297 298 /** 299 * Builds the immutable context. 300 * 301 * @return A new context. 302 * @throws IllegalStateException If a required setting is missing or two settings conflict. 303 */ 304 public abstract BeanQueryContext<T> build(); 305 } 306 307 final QuerySettings settings; 308 309 /** 310 * Constructor. Takes a private copy of the builder's settings and validates it. 311 * 312 * @param builder The builder. Must not be <jk>null</jk>. 313 * @throws IllegalStateException If the settings are invalid. 314 */ 315 protected BeanQueryContext(Builder<T,?> builder) { 316 settings = reqnn("builder", builder).settings.copy(); 317 settings.validate(); 318 } 319 320 /** 321 * Returns the declared columns, in order: the allow-list for search, sort and view, and the default view. 322 * 323 * @return An unmodifiable list. 324 */ 325 public final List<String> columns() { 326 return List.copyOf(settings.columns.keySet()); 327 } 328 329 /** 330 * Returns the default operator set a client may use. {@code $regex} is absent if regex is disallowed. 331 * 332 * @return The operator set. 333 */ 334 public final SearchOperatorSet operatorSet() { 335 return visible(settings.defaultOperators); 336 } 337 338 /** 339 * Returns the operator set a client may use on one column. {@code $regex} is absent if regex is disallowed. 340 * 341 * @param column The column name. 342 * @return The column's operator set, else the default set. 343 */ 344 public final SearchOperatorSet operatorSet(String column) { 345 return visible(settings.operators(column)); 346 } 347 348 private SearchOperatorSet visible(SearchOperatorSet s) { 349 return settings.allowRegex ? s : s.without("$regex"); 350 } 351 352 /** 353 * Returns the type stored for a declared column: the type given to {@link Builder#column(String, SearchType) 354 * column}, or the type inferred for it (for example, {@link InMemoryBeanQueryContext#create(Class) 355 * InMemoryBeanQueryContext.create(Class)} infers one per bean getter). 356 * 357 * @param name The column name. 358 * @return The column's type. Never <jk>null</jk>. 359 * @throws IllegalArgumentException If the column is not declared. 360 */ 361 public final SearchType getColumnType(String name) { 362 if (! settings.columns.containsKey(name)) 363 throw iaex("Unknown column '%s'.", name); 364 return settings.columns.get(name); 365 } 366 367 /** 368 * Returns the largest number of search clauses a query may use. 369 * 370 * @return The cap. 371 */ 372 public final int getMaxSearchClauses() { 373 return settings.maxSearchClauses; 374 } 375 376 /** 377 * Returns the largest number of sort keys a query may use. 378 * 379 * @return The cap. 380 */ 381 public final int getMaxSortKeys() { 382 return settings.maxSortKeys; 383 } 384 385 /** 386 * Validates a query against this context's rules without running it. 387 * 388 * @param query The query. Must not be <jk>null</jk>. 389 * @return The resolved query. 390 * @throws BeanQuerySyntaxException If the query breaks a rule. 391 */ 392 public final ResolvedQuery resolve(BeanQuery query) { 393 return QueryResolver.resolve(reqnn("query", query), settings, this); 394 } 395 396 /** 397 * Confirms that {@code query} was produced by resolving against this context (directly, or through a session 398 * built from it) before an engine uses it. An engine's rendering/execution code that accepts a caller-supplied 399 * {@link ResolvedQuery} — rather than one it just resolved itself — calls this first, so a query 400 * resolved against a different context's columns is rejected with a clear message instead of silently rendering 401 * (or running) against the wrong schema. 402 * 403 * @param query The query to check. Must not be <jk>null</jk>. 404 * @throws IllegalArgumentException If {@code query} was not produced by this context. 405 */ 406 protected final void checkOrigin(ResolvedQuery query) { 407 req(query.isResolvedBy(this), "This ResolvedQuery was not produced by this context (resolve it via this context, or a session built from it)."); 408 } 409 410 /** 411 * Returns a builder initialized from this context. Building it leaves this context unchanged. 412 * 413 * @return A new builder. 414 */ 415 public abstract Builder<T,?> copy(); 416 417 /** 418 * Returns a builder for a session over this context. 419 * 420 * @return A new session builder. 421 */ 422 public abstract BeanQuerySession.Builder<T,?> createSession(); 423}