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 — 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<Person> 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}