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&lt;Person&gt; <jv>session</jv> = <jv>context</jv>.getSession(<jv>rows</jv>)) {
045 *       Page&lt;Person&gt; <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&lt;Person&gt; <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&lt;Map&lt;String,Object&gt;&gt; <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}