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.*; 023import java.util.function.*; 024import java.util.stream.*; 025 026/** 027 * One unit of query work against a {@link BeanQueryContext}: the rows (or connection) to query, plus optional 028 * per-request narrowing of the context's settings. 029 * 030 * <p> 031 * Open a session per request and close it with try-with-resources. A session is not thread-safe. Its public methods 032 * are {@code final}: each validates the {@link BeanQuery} into a {@link ResolvedQuery} (throwing 033 * {@link BeanQuerySyntaxException} for bad input) and then calls the engine's {@code doXxx} hook. Engine failures 034 * throw {@link BeanQueryExecutionException}. 035 * 036 * <p> 037 * {@link Builder#restrictColumns(String...)} only narrows the context's columns. The limit, cap and count-policy 038 * overrides replace the context's values for this session (so they can also raise or remove a cap); they are for 039 * trusted server-side callers only, never request data. 040 * 041 * <h5 class='section'>Example:</h5> 042 * <p class='bjava'> 043 * <jc>// Shortcut: the context's settings as-is.</jc> 044 * <jk>try</jk> (InMemoryBeanQuerySession<Person> <jv>session</jv> = <jv>context</jv>.getSession(<jv>rows</jv>)) { 045 * Page<Person> <jv>page</jv> = <jv>session</jv>.find(<jv>query</jv>); 046 * } 047 * 048 * <jc>// Narrowed for a guest user.</jc> 049 * <jk>try</jk> (InMemoryBeanQuerySession<Person> <jv>session</jv> = <jv>context</jv>.createSession() 050 * .rows(<jv>rows</jv>) 051 * .restrictColumns(<js>"name"</js>, <js>"city"</js>) 052 * .defaultLimit(20) 053 * .maxLimit(20) 054 * .build()) { 055 * List<Map<String,Object>> <jv>values</jv> = <jv>session</jv>.findValues(<jv>query</jv>).rows(); 056 * } 057 * </p> 058 * 059 * @param <T> The row (bean) type. 060 * @since 10.0.0 061 */ 062public abstract class BeanQuerySession<T> implements AutoCloseable { 063 064 /** 065 * The settings every engine's session builder shares. Each starts from the context's value. 066 * 067 * @param <T> The row type. 068 * @param <SELF> The concrete builder type, returned by every setter. 069 */ 070 @SuppressWarnings({ 071 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 072 }) 073 public abstract static class Builder<T,SELF extends Builder<T,SELF>> { 074 075 private final BeanQueryContext<T> context; 076 final QuerySettings settings; 077 private boolean defaultLimitExplicit; // True once defaultLimit(int) is called on THIS builder. 078 079 /** 080 * Constructor. 081 * 082 * @param context The context the session runs against. Must not be <jk>null</jk>. 083 */ 084 protected Builder(BeanQueryContext<T> context) { 085 this.context = reqnn("context", context); 086 settings = this.context.settings.copy(); 087 } 088 089 /** 090 * Returns this builder as its concrete type. 091 * 092 * @return This object. 093 */ 094 @SuppressWarnings({ 095 "unchecked" // CRTP self-type cast is safe by construction. 096 }) 097 protected final SELF self() { 098 return (SELF)this; 099 } 100 101 /** 102 * Keeps only the specified columns for this session. The context's column order is kept. 103 * 104 * @param names The column names. Each must be a column of the context (or of an earlier restriction). 105 * @return This object. 106 * @throws IllegalArgumentException If a name is not a current column. 107 */ 108 public SELF restrictColumns(String...names) { 109 var keep = new HashSet<String>(); 110 for (var n : reqnns("names", names)) { 111 if (! settings.columns.containsKey(n)) 112 throw iaex("Column '%s' is not a column of this context; a session can only narrow the columns.", n); 113 keep.add(n); 114 } 115 settings.columns.keySet().retainAll(keep); 116 settings.columnOperators.keySet().retainAll(keep); 117 return self(); 118 } 119 120 /** 121 * Overrides {@link BeanQueryContext.Builder#regexTimeout(Duration)} for this session. 122 * 123 * @param value The budget. Must be positive. 124 * @return This object. 125 */ 126 public SELF regexTimeout(Duration value) { 127 settings.regexTimeout = QuerySettings.positive("regexTimeout", value); 128 return self(); 129 } 130 131 /** 132 * Overrides {@link BeanQueryContext.Builder#countPolicy(CountPolicy)} for this session. 133 * 134 * @param value The policy. Must not be <jk>null</jk>. 135 * @return This object. 136 */ 137 public SELF countPolicy(CountPolicy value) { 138 settings.countPolicy = reqnn("value", value); 139 return self(); 140 } 141 142 /** 143 * Overrides {@link BeanQueryContext.Builder#defaultLimit(int)} for this session. 144 * 145 * @param value The limit. Must be positive. 146 * @return This object. 147 */ 148 public SELF defaultLimit(int value) { 149 settings.defaultLimit = QuerySettings.positive("defaultLimit", value); 150 defaultLimitExplicit = true; 151 return self(); 152 } 153 154 /** 155 * Overrides {@link BeanQueryContext.Builder#maxLimit(Integer)} for this session. 156 * 157 * <p> 158 * If this session's {@code defaultLimit} was never explicitly set on this builder (it is still the value 159 * inherited from the context), narrowing {@code maxLimit} below it auto-narrows {@code defaultLimit} to match, 160 * rather than leaving the two in conflict. An explicitly-chosen {@code defaultLimit} is never 161 * silently changed by this setter — a resulting conflict is left for {@link #build()} to reject. 162 * 163 * @param value The cap, or <jk>null</jk> for no cap. Must be positive if set. 164 * @return This object. 165 */ 166 public SELF maxLimit(Integer value) { 167 settings.maxLimit = QuerySettings.positiveOrNull("maxLimit", value); 168 if (! defaultLimitExplicit && settings.maxLimit != null && settings.defaultLimit > settings.maxLimit) 169 settings.defaultLimit = settings.maxLimit; 170 return self(); 171 } 172 173 /** 174 * Overrides {@link BeanQueryContext.Builder#maxSearchClauses(int)} for this session. 175 * 176 * @param value The cap. Must be positive. 177 * @return This object. 178 */ 179 public SELF maxSearchClauses(int value) { 180 settings.maxSearchClauses = QuerySettings.positive("maxSearchClauses", value); 181 return self(); 182 } 183 184 /** 185 * Overrides {@link BeanQueryContext.Builder#maxSortKeys(int)} for this session. 186 * 187 * @param value The cap. Must be positive. 188 * @return This object. 189 */ 190 public SELF maxSortKeys(int value) { 191 settings.maxSortKeys = QuerySettings.positive("maxSortKeys", value); 192 return self(); 193 } 194 195 /** 196 * Overrides {@link BeanQueryContext.Builder#maxSearchLength(int)} for this session. 197 * 198 * @param value The cap. Must be positive. 199 * @return This object. 200 */ 201 public SELF maxSearchLength(int value) { 202 settings.maxSearchLength = QuerySettings.positive("maxSearchLength", value); 203 return self(); 204 } 205 206 /** 207 * Overrides {@link BeanQueryContext.Builder#maxExpressionDepth(int)} for this session. 208 * 209 * @param value The cap. Must be positive. 210 * @return This object. 211 */ 212 public SELF maxExpressionDepth(int value) { 213 settings.maxExpressionDepth = QuerySettings.positive("maxExpressionDepth", value); 214 return self(); 215 } 216 217 /** 218 * Builds the session. 219 * 220 * @return A new session. Close it when done. 221 * @throws IllegalStateException If a required setting is missing or two settings conflict. 222 */ 223 public abstract BeanQuerySession<T> build(); 224 } 225 226 private final BeanQueryContext<T> context; 227 final QuerySettings settings; 228 229 /** 230 * Constructor. Takes a private copy of the builder's settings and validates it. 231 * 232 * @param builder The builder. Must not be <jk>null</jk>. 233 * @throws IllegalStateException If the settings are invalid. 234 */ 235 protected BeanQuerySession(Builder<T,?> builder) { 236 context = reqnn("builder", builder).context; 237 settings = builder.settings.copy(); 238 settings.validate(); 239 } 240 241 /** 242 * Returns this session's columns, in order (the context's columns after any {@link Builder#restrictColumns 243 * restriction}). 244 * 245 * @return An unmodifiable list. 246 */ 247 public final List<String> columns() { 248 return List.copyOf(settings.columns.keySet()); 249 } 250 251 /** 252 * Returns the context that created this session (through {@link BeanQueryContext#createSession()} or an 253 * engine's {@code getSession(...)} shortcut). 254 * 255 * <p> 256 * Engines whose session builder is given a narrower context type (for example, 257 * {@code InMemoryBeanQuerySession}) override this with a covariant return type. 258 * 259 * @return The context. Never <jk>null</jk>. 260 */ 261 public BeanQueryContext<T> getContext() { 262 return context; 263 } 264 265 /** 266 * Returns the largest number of search clauses a query may use in this session: the context's cap, or this 267 * session's {@link Builder#maxSearchClauses(int) override}. 268 * 269 * @return The cap. 270 */ 271 public final int getMaxSearchClauses() { 272 return settings.maxSearchClauses; 273 } 274 275 /** 276 * Returns the largest number of sort keys a query may use in this session: the context's cap, or this session's 277 * {@link Builder#maxSortKeys(int) override}. 278 * 279 * @return The cap. 280 */ 281 public final int getMaxSortKeys() { 282 return settings.maxSortKeys; 283 } 284 285 /** 286 * Validates a query against this session's rules without running it. 287 * 288 * @param query The query. Must not be <jk>null</jk>. 289 * @return The resolved query. 290 * @throws BeanQuerySyntaxException If the query breaks a rule. 291 */ 292 public final ResolvedQuery resolve(BeanQuery query) { 293 return QueryResolver.resolve(reqnn("query", query), settings, context); 294 } 295 296 /** 297 * Returns one page of matching rows, plus the counts the {@link CountPolicy} selects. 298 * 299 * @param query The query. Must not be <jk>null</jk>. 300 * @return The page. 301 * @throws BeanQuerySyntaxException If the query breaks a rule. 302 * @throws BeanQueryExecutionException If the engine fails. 303 */ 304 public final Page<T> find(BeanQuery query) { 305 return doFind(resolve(query)); 306 } 307 308 /** 309 * Like {@link #find(BeanQuery)}, but each row is a map of the viewed columns, in view order. 310 * 311 * @param query The query. Must not be <jk>null</jk>. 312 * @return The page. 313 * @throws BeanQuerySyntaxException If the query breaks a rule. 314 * @throws BeanQueryExecutionException If the engine fails. 315 */ 316 public final Page<Map<String,Object>> findValues(BeanQuery query) { 317 return doFindValues(resolve(query)); 318 } 319 320 /** 321 * Returns both counts, whatever the {@link CountPolicy}. Sort, view and paging are ignored. 322 * 323 * @param query The query. Must not be <jk>null</jk>. 324 * @return The counts. 325 * @throws BeanQuerySyntaxException If the query breaks a rule. 326 * @throws BeanQueryExecutionException If the engine fails. 327 */ 328 public final Counts count(BeanQuery query) { 329 return doCount(resolve(query)); 330 } 331 332 /** 333 * Streams one page of matching rows. Close the stream (or the session) to release engine resources. 334 * 335 * @param query The query. Must not be <jk>null</jk>. 336 * @return The rows. 337 * @throws BeanQuerySyntaxException If the query breaks a rule. 338 * @throws BeanQueryExecutionException If the engine fails. 339 */ 340 public final Stream<T> stream(BeanQuery query) { 341 return doStream(resolve(query)); 342 } 343 344 /** 345 * Like {@link #stream(BeanQuery)}, but each row is a map of the viewed columns, in view order. 346 * 347 * @param query The query. Must not be <jk>null</jk>. 348 * @return The rows. 349 * @throws BeanQuerySyntaxException If the query breaks a rule. 350 * @throws BeanQueryExecutionException If the engine fails. 351 */ 352 public final Stream<Map<String,Object>> streamValues(BeanQuery query) { 353 return doStreamValues(resolve(query)); 354 } 355 356 /** 357 * Engine hook for {@link #find(BeanQuery)}. 358 * 359 * @param query The validated query. 360 * @return The page. Build it with {@link #page(List, ResolvedQuery, LongSupplier, LongSupplier)}. 361 */ 362 protected abstract Page<T> doFind(ResolvedQuery query); 363 364 /** 365 * Engine hook for {@link #findValues(BeanQuery)}. 366 * 367 * @param query The validated query. 368 * @return The page. 369 */ 370 protected abstract Page<Map<String,Object>> doFindValues(ResolvedQuery query); 371 372 /** 373 * Engine hook for {@link #count(BeanQuery)}. 374 * 375 * @param query The validated query. 376 * @return The counts. 377 */ 378 protected abstract Counts doCount(ResolvedQuery query); 379 380 /** 381 * Engine hook for {@link #stream(BeanQuery)}. 382 * 383 * @param query The validated query. 384 * @return The rows. 385 */ 386 protected abstract Stream<T> doStream(ResolvedQuery query); 387 388 /** 389 * Engine hook for {@link #streamValues(BeanQuery)}. 390 * 391 * @param query The validated query. 392 * @return The rows. 393 */ 394 protected abstract Stream<Map<String,Object>> doStreamValues(ResolvedQuery query); 395 396 /** 397 * Builds a page, computing only the counts the query's {@link ResolvedQuery#counts() policy} selects. 398 * 399 * @param <R> The row type. 400 * @param rows The page's rows. 401 * @param query The validated query. 402 * @param total Computes the total count. Called only under {@link CountPolicy#BOTH}. 403 * @param matched Computes the matched count. Called only under {@link CountPolicy#MATCHED} or {@link CountPolicy#BOTH}. 404 * @return The page. 405 */ 406 protected static <R> Page<R> page(List<R> rows, ResolvedQuery query, LongSupplier total, LongSupplier matched) { 407 switch (query.counts()) { 408 case BOTH: return Page.of(rows, total.getAsLong(), matched.getAsLong()); 409 case MATCHED: return Page.ofMatched(rows, matched.getAsLong()); 410 default: return Page.of(rows); 411 } 412 } 413 414 /** 415 * Releases whatever the session acquired. Never throws a checked exception. 416 */ 417 @Override /* AutoCloseable */ 418 public abstract void close(); 419}