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.rest.server.converter;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.time.*;
022
023import org.apache.juneau.commons.beanquery.*;
024
025/**
026 * Per-resource limits and defaults for the {@link Queryable} converter.
027 *
028 * <p>
029 * A resource tunes {@link Queryable}'s BeanQuery engine by registering a {@code QueryableSettings} bean in its bean
030 * store (mirrors the {@link IntrospectableSettings} idiom).  When no such bean is present, {@link Queryable} uses
031 * {@link #DEFAULT}, which sets nothing and so leaves every setting at the context builder's own default.
032 *
033 * <p>
034 * The defaults live in {@link org.apache.juneau.commons.beanquery.BeanQueryContext.Builder BeanQueryContext.Builder}
035 * only &mdash; this class never duplicates them.  Each setting
036 * is tracked as set or unset; {@link #applyTo(BeanQueryContext.Builder) applyTo} copies only the ones that were set,
037 * so an unset setting stays at the builder default (currently: {@code allowRegex} true, {@code regexTimeout} 50ms,
038 * {@code countPolicy} {@link CountPolicy#IF_REQUESTED IF_REQUESTED}, {@code defaultLimit} 100, {@code maxLimit} 1000,
039 * and caps of 64 search clauses, 8 sort keys, 4096 search characters and 16 expression levels).
040 *
041 * <p>
042 * Every setting here is a trusted, server-side cap or default; none of them is ever set from request data.
043 *
044 * <h5 class='section'>Example:</h5>
045 * <p class='bjava'>
046 *    <ja>@Rest</ja>(converters=Queryable.<jk>class</jk>)
047 *    <jk>public class</jk> PeopleResource <jk>extends</jk> BasicRestServlet {
048 *
049 *       <ja>@Bean</ja>
050 *       <jk>public</jk> QueryableSettings queryableSettings() {
051 *          <jc>// allowRegex defaults to true; call allowRegex(false) here to opt this resource out.</jc>
052 *          <jk>return</jk> QueryableSettings.<jsm>create</jsm>().defaultLimit(20).maxLimit(500).build();
053 *       }
054 *
055 *       <ja>@RestGet</ja>(<js>"/people"</js>)
056 *       <jk>public</jk> List&lt;Person&gt; getPeople() {
057 *          <jk>return</jk> <jf>people</jf>;
058 *       }
059 *    }
060 * </p>
061 *
062 * <h5 class='section'>See Also:</h5><ul>
063 *    <li class='jc'>{@link Queryable}
064 *    <li class='jc'>{@link org.apache.juneau.commons.beanquery.BeanQueryContext.Builder BeanQueryContext.Builder}
065 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Converters">Converters</a>
066 * </ul>
067 *
068 * @since 10.0.0
069 */
070public class QueryableSettings {
071
072   /** The default settings used when no bean is registered: nothing set, so every builder default applies. */
073   public static final QueryableSettings DEFAULT = create().build();
074
075   private final Boolean allowRegex;
076   private final Duration regexTimeout;
077   private final CountPolicy countPolicy;
078   private final Integer defaultLimit;
079   private final boolean maxLimitSet;
080   private final Integer maxLimit;
081   private final Integer maxSearchClauses;
082   private final Integer maxSortKeys;
083   private final Integer maxSearchLength;
084   private final Integer maxExpressionDepth;
085
086   private QueryableSettings(Builder b) {
087      this.allowRegex = b.allowRegex;
088      this.regexTimeout = b.regexTimeout;
089      this.countPolicy = b.countPolicy;
090      this.defaultLimit = b.defaultLimit;
091      this.maxLimitSet = b.maxLimitSet;
092      this.maxLimit = b.maxLimit;
093      this.maxSearchClauses = b.maxSearchClauses;
094      this.maxSortKeys = b.maxSortKeys;
095      this.maxSearchLength = b.maxSearchLength;
096      this.maxExpressionDepth = b.maxExpressionDepth;
097   }
098
099   /**
100    * Builder creator.
101    *
102    * @return A new builder with nothing set.
103    */
104   public static Builder create() {
105      return new Builder();
106   }
107
108   /**
109    * Creates a builder pre-populated with these settings.
110    *
111    * @return A new builder.
112    */
113   public Builder copy() {
114      var b = new Builder();
115      b.allowRegex = allowRegex;
116      b.regexTimeout = regexTimeout;
117      b.countPolicy = countPolicy;
118      b.defaultLimit = defaultLimit;
119      b.maxLimitSet = maxLimitSet;
120      b.maxLimit = maxLimit;
121      b.maxSearchClauses = maxSearchClauses;
122      b.maxSortKeys = maxSortKeys;
123      b.maxSearchLength = maxSearchLength;
124      b.maxExpressionDepth = maxExpressionDepth;
125      return b;
126   }
127
128   /**
129    * Copies every setting that was set onto a context builder.
130    *
131    * <p>
132    * Settings that were not set are left untouched, so the builder's own defaults apply.
133    *
134    * @param <B> The context builder type.
135    * @param builder The builder to copy onto.  Must not be <jk>null</jk>.
136    * @return The same builder, for chaining.
137    * @throws IllegalArgumentException If a value is rejected by the builder (for example a non-positive limit).
138    */
139   public <B extends BeanQueryContext.Builder<?,B>> B applyTo(B builder) {
140      reqnn("builder", builder);
141      // maxLimit goes before defaultLimit so that narrowing maxLimit can auto-narrow an unset defaultLimit, while an
142      // explicit defaultLimit is never silently changed.
143      if (maxLimitSet)
144         builder.maxLimit(maxLimit);
145      if (defaultLimit != null)
146         builder.defaultLimit(defaultLimit);
147      if (allowRegex != null)
148         builder.allowRegex(allowRegex);
149      if (regexTimeout != null)
150         builder.regexTimeout(regexTimeout);
151      if (countPolicy != null)
152         builder.countPolicy(countPolicy);
153      if (maxSearchClauses != null)
154         builder.maxSearchClauses(maxSearchClauses);
155      if (maxSortKeys != null)
156         builder.maxSortKeys(maxSortKeys);
157      if (maxSearchLength != null)
158         builder.maxSearchLength(maxSearchLength);
159      if (maxExpressionDepth != null)
160         builder.maxExpressionDepth(maxExpressionDepth);
161      return builder;
162   }
163
164   /**
165    * Builder for {@link QueryableSettings}.
166    *
167    * <p>
168    * Each setter records a value to be copied by {@link QueryableSettings#applyTo(BeanQueryContext.Builder)}; the
169    * values are validated by the context builder when applied.
170    */
171   public static class Builder {
172      Boolean allowRegex;
173      Duration regexTimeout;
174      CountPolicy countPolicy;
175      Integer defaultLimit;
176      boolean maxLimitSet;
177      Integer maxLimit;
178      Integer maxSearchClauses;
179      Integer maxSortKeys;
180      Integer maxSearchLength;
181      Integer maxExpressionDepth;
182
183      Builder() {}
184
185      /**
186       * Enables or disables the {@code $regex} search operator.
187       *
188       * @param value <jk>true</jk> to allow {@code $regex}.  Builder default is <jk>true</jk>; call
189       *    {@code allowRegex(false)} to opt out.
190       * @return This object.
191       */
192      public Builder allowRegex(boolean value) {
193         this.allowRegex = value;
194         return this;
195      }
196
197      /**
198       * Sets the time budget for all {@code $regex} matching in one query.
199       *
200       * @param value The budget.  Must be positive.
201       * @return This object.
202       */
203      public Builder regexTimeout(Duration value) {
204         this.regexTimeout = reqnn("value", value);
205         return this;
206      }
207
208      /**
209       * Sets which counts are computed.
210       *
211       * @param value The policy.  Must not be <jk>null</jk>.
212       * @return This object.
213       */
214      public Builder countPolicy(CountPolicy value) {
215         this.countPolicy = reqnn("value", value);
216         return this;
217      }
218
219      /**
220       * Sets the page size used when the request specifies no limit.
221       *
222       * @param value The default limit.  Must be positive.
223       * @return This object.
224       */
225      public Builder defaultLimit(int value) {
226         this.defaultLimit = value;
227         return this;
228      }
229
230      /**
231       * Sets the largest page size a request may ask for; larger limits are clamped to it.
232       *
233       * @param value The cap, or <jk>null</jk> for no cap.  Must be positive if set.
234       * @return This object.
235       */
236      public Builder maxLimit(Integer value) {
237         this.maxLimitSet = true;
238         this.maxLimit = value;
239         return this;
240      }
241
242      /**
243       * Sets the largest number of search clauses.
244       *
245       * @param value The cap.  Must be positive.
246       * @return This object.
247       */
248      public Builder maxSearchClauses(int value) {
249         this.maxSearchClauses = value;
250         return this;
251      }
252
253      /**
254       * Sets the largest number of sort keys.
255       *
256       * @param value The cap.  Must be positive.
257       * @return This object.
258       */
259      public Builder maxSortKeys(int value) {
260         this.maxSortKeys = value;
261         return this;
262      }
263
264      /**
265       * Sets the longest search, sort, view or opts string, in characters.
266       *
267       * @param value The cap.  Must be positive.
268       * @return This object.
269       */
270      public Builder maxSearchLength(int value) {
271         this.maxSearchLength = value;
272         return this;
273      }
274
275      /**
276       * Sets the deepest allowed expression nesting.
277       *
278       * @param value The cap.  Must be positive.
279       * @return This object.
280       */
281      public Builder maxExpressionDepth(int value) {
282         this.maxExpressionDepth = value;
283         return this;
284      }
285
286      /**
287       * Builds the (immutable) settings.
288       *
289       * @return A new {@link QueryableSettings}.
290       */
291      public QueryableSettings build() {
292         return new QueryableSettings(this);
293      }
294   }
295}