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&lt;Person&gt; <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&lt;Person&gt; <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&lt;Person&gt; <jv>session</jv> = <jv>people</jv>.getSession(<jv>rows</jv>)) {
052 *       Page&lt;Person&gt; <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} &mdash; rather than one it just resolved itself &mdash; 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}