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.util.*; 022 023/** 024 * The immutable definition of a single {@code $}-search operator — its {@code $}-name, arity, the value types 025 * it applies to, and its popup help text. 026 * 027 * <p> 028 * A custom operator always carries its own help text; the help text of a built-in ({@link SearchOperatorSet#standard()}) 029 * operator is owned by the browser runtime, so {@link #help()} is <jk>null</jk> for built-ins. A custom operator 030 * additionally may carry a {@link SearchPredicate} the in-memory context calls when it hits 031 * that {@code $}-name — Juneau never invents one. Anything else a downstream module (for example a SQL 032 * renderer) needs to attach lives in the typed {@link #extension(Class) extension} slot: it is keyed by the 033 * extension's own {@link Class}, so this module (commons) never needs to depend on that module's types (design 034 * §5.3). 035 * 036 * <p> 037 * Instances are built with {@link Builder}: {@link #create(String, String)} starts a new custom operator, and 038 * {@link #copy()} starts a modified copy of an existing one (built-in or custom). 039 * 040 * <h5 class='section'>Combinators vs. leaves</h5> 041 * <p> 042 * {@code $and}/{@code $or}/{@code $not} are {@link #isCombinator() combinators}: their children are sub-expressions, 043 * not literal value arguments, and they apply to every type. Every other operator is a leaf whose arguments are 044 * literal values and whose {@link #appliesTo(SearchType) applicability} is a fixed type set (an <b>empty</b> type set 045 * means "applies to all types"). 046 * 047 * <h5 class='section'>Example (custom operator, with a typed extension):</h5> 048 * <p class='bjava'> 049 * SearchOperator <jv>op</jv> = SearchOperator.<jsm>create</jsm>(<js>"$near"</js>, <js>"Fuzzy match. Example: $near(jhon)"</js>) 050 * .types(SearchType.<jsf>TEXT</jsf>) 051 * .predicate((<jv>cell</jv>, <jv>args</jv>) -> <jsm>fuzzy</jsm>(<jv>cell</jv>, <jv>args</jv>.get(0))) 052 * .extension(MySqlRenderer.<jk>class</jk>, <jv>myRenderer</jv>) 053 * .build(); 054 * 055 * <jc>// A later consumer that knows about MySqlRenderer retrieves it back, type-safe, with no cast.</jc> 056 * Optional<MySqlRenderer> <jv>renderer</jv> = <jv>op</jv>.extension(MySqlRenderer.<jk>class</jk>); 057 * </p> 058 * 059 * @since 10.0.0 060 */ 061public final class SearchOperator { 062 063 private final String name; 064 private final String help; 065 private final int minArgs; 066 private final int maxArgs; 067 private final Set<SearchType> types; 068 private final SearchPredicate predicate; 069 private final boolean custom; 070 private final boolean combinator; 071 private final boolean typedArgs; 072 private final Map<Class<?>,Object> extensions; 073 074 private SearchOperator(Builder b) { 075 name = b.name; 076 help = b.help; 077 minArgs = b.minArgs; 078 maxArgs = b.maxArgs; 079 types = Set.copyOf(b.types); 080 predicate = b.predicate; 081 custom = b.custom; 082 combinator = b.combinator; 083 typedArgs = b.typedArgs; 084 extensions = u(new LinkedHashMap<>(b.extensions)); 085 } 086 087 /** 088 * Creates a built-in operator definition. Package-private — the canonical set is 089 * {@link SearchOperatorSet#standard()}; applications add operators through {@link #create(String, String)}. 090 */ 091 static SearchOperator builtin(String name, int minArgs, int maxArgs, boolean combinator, SearchType...types) { 092 var b = new Builder(); 093 b.name = name; 094 b.minArgs = minArgs; 095 b.maxArgs = maxArgs; 096 b.custom = false; 097 b.combinator = combinator; 098 b.types = types == null || types.length == 0 ? new LinkedHashSet<>() : new LinkedHashSet<>(l(types)); 099 return b.build(); 100 } 101 102 /** 103 * Starts a new <b>custom</b> (application-defined) leaf operator. 104 * 105 * <p> 106 * Defaults: arity {@code 1..*} (one or more arguments), applies to <b>all</b> value types (an empty type set), 107 * no predicate, no extensions. Narrow it with {@link Builder#minArgs(int)}/{@link Builder#maxArgs(int)}/ 108 * {@link Builder#types(SearchType...)} and attach an evaluator with {@link Builder#predicate(SearchPredicate)} 109 * (in-memory) and/or a typed {@link Builder#extension(Class, Object)} (for example, a SQL renderer). 110 * 111 * @param name The operator name, including the leading {@code $} (e.g. {@code "$near"}). Must not be 112 * <jk>null</jk> or blank and must start with {@code $}. 113 * @param helpText The popup help text: what the operator accepts and a short example. Must not be 114 * <jk>null</jk> or blank. 115 * @return A new {@link Builder} pre-filled with {@code name}/{@code helpText} and the defaults above. 116 * @throws IllegalArgumentException If {@code name} is not a valid {@code $}-name or {@code helpText} is blank. 117 */ 118 public static Builder create(String name, String helpText) { 119 req(inb(name) && name.startsWith("$") && name.length() >= 2, 120 "SearchOperator name must start with '$' and not be blank: '%s'", name); 121 req(inb(helpText), "SearchOperator help text must not be null or blank (operator '%s').", name); 122 var b = new Builder(); 123 b.name = name; 124 b.help = helpText; 125 b.custom = true; 126 return b; 127 } 128 129 /** 130 * Starts a {@link Builder} pre-filled with this operator's current state, for building a modified copy. 131 * 132 * <p> 133 * The returned builder is fully independent of this instance: mutating it (or the {@link SearchOperator} it 134 * eventually {@link Builder#build() builds}) never affects this instance. 135 * 136 * <p> 137 * <b>Renaming is only legal for a copy of a <i>custom</i> operator.</b> Both evaluation engines dispatch a 138 * built-in purely by its {@link #name()} — the in-memory engine's leaf switch and 139 * {@code SqlDialect#builtinRenderer(String)} alike — so {@link Builder#name(String) changing the name} on a 140 * copy of a <i>custom</i> operator (one with its own {@link Builder#predicate(SearchPredicate) predicate}/ 141 * {@link Builder#extension(Class, Object) extension}) is fine and gives it a fresh identity, but doing the same 142 * to a copy of a <b>built-in</b> operator throws {@link IllegalArgumentException} from {@link Builder#build() 143 * build()}. A built-in's copy may still have its {@link Builder#help(String) help text} changed freely; only its 144 * name is fixed. 145 * 146 * @return A new builder seeded from this operator. 147 */ 148 public Builder copy() { 149 var b = new Builder(); 150 b.name = name; 151 b.help = help; 152 b.minArgs = minArgs; 153 b.maxArgs = maxArgs; 154 b.types = new LinkedHashSet<>(types); 155 b.predicate = predicate; 156 b.custom = custom; 157 b.combinator = combinator; 158 b.typedArgs = typedArgs; 159 b.extensions = new LinkedHashMap<>(extensions); 160 if (! custom) 161 b.builtinOriginalName = name; 162 return b; 163 } 164 165 /** 166 * The operator name, including the leading {@code $} (e.g. {@code "$eq"}). 167 * 168 * @return The operator name. 169 */ 170 public String name() { 171 return name; 172 } 173 174 /** 175 * The human-readable help text for this operator, shown in the column-search popup. 176 * 177 * <p> 178 * Built-in operators return <jk>null</jk>: their help text is owned by <c>juneau-search.js</c>, which the popup 179 * falls back to. Custom operators always return non-blank text, because {@link Builder#build()} rejects a custom 180 * operator without it. 181 * 182 * <h5 class='section'>Example:</h5> 183 * <p class='bjava'> 184 * SearchOperator <jv>near</jv> = SearchOperator.<jsm>create</jsm>(<js>"$near"</js>, <js>"Within N units. Example: $near(5)"</js>).build(); 185 * <jv>near</jv>.help(); <jc>// "Within N units. Example: $near(5)"</jc> 186 * SearchOperatorSet.<jsm>standard</jsm>().get(<js>"$eq"</js>).help(); <jc>// null</jc> 187 * </p> 188 * 189 * @return The help text, or <jk>null</jk> for a built-in operator. 190 */ 191 public String help() { 192 return help; 193 } 194 195 /** 196 * Whether this is an application-defined custom operator (vs. a built-in). 197 * 198 * @return <jk>true</jk> if custom. 199 */ 200 public boolean isCustom() { 201 return custom; 202 } 203 204 /** 205 * Whether this is a combinator ({@code $and}/{@code $or}/{@code $not}) whose children are sub-expressions. 206 * 207 * @return <jk>true</jk> if a combinator. 208 */ 209 public boolean isCombinator() { 210 return combinator; 211 } 212 213 /** 214 * The minimum argument count (inclusive). 215 * 216 * @return The minimum arity. 217 */ 218 public int minArgs() { 219 return minArgs; 220 } 221 222 /** 223 * The maximum argument count (inclusive), or {@code -1} for unbounded. 224 * 225 * @return The maximum arity, or {@code -1}. 226 */ 227 public int maxArgs() { 228 return maxArgs; 229 } 230 231 /** 232 * The in-memory evaluator for a custom operator, if one was supplied. 233 * 234 * @return The predicate, or <jk>null</jk> for built-ins and for customs with no predicate. 235 */ 236 public SearchPredicate predicate() { 237 return predicate; 238 } 239 240 /** 241 * The value types this operator applies to (combinators apply to all types regardless of this set). 242 * 243 * @return An unmodifiable view of the applicable types; empty means "applies to all types". 244 */ 245 public Set<SearchType> types() { 246 return types; 247 } 248 249 /** 250 * Whether this operator applies to the specified value type. 251 * 252 * @param type The column value type. Can be <jk>null</jk>. 253 * @return <jk>true</jk> if this operator is offered for that type (combinators, and operators with an empty 254 * {@link #types() type set}, apply to every type). 255 */ 256 public boolean appliesTo(SearchType type) { 257 return type != null && (combinator || types.isEmpty() || types.contains(type)); 258 } 259 260 /** 261 * Whether the specified argument count satisfies this operator's arity. 262 * 263 * @param count The number of arguments supplied. 264 * @return <jk>true</jk> if {@code count} is within {@code [minArgs, maxArgs]} (upper bound ignored when unbounded). 265 */ 266 public boolean acceptsArgCount(int count) { 267 return count >= minArgs && (maxArgs < 0 || count <= maxArgs); 268 } 269 270 /** 271 * Whether this operator's SQL renderer should be given {@link SearchExpression#typedArgs() typed values} rather 272 * than {@link SearchExpression#literalArgs() literal strings}. Never affects the in-memory 273 * {@link #predicate() predicate}, which always receives literal strings. 274 * 275 * @return <jk>true</jk> if typed (see {@link Builder#typedArgs(boolean)}). 276 */ 277 public boolean typedArgs() { 278 return typedArgs; 279 } 280 281 /** 282 * Looks up a typed extension previously attached with {@link Builder#extension(Class, Object)}. 283 * 284 * <p> 285 * This is how a downstream module (for example {@code juneau-beanquery-sql}'s SQL renderer) attaches its own 286 * metadata to a custom operator without this module (commons) ever depending on that module's types: the 287 * extension is keyed by its own {@link Class}, so retrieval is type-safe with no cast (design §5.3). 288 * 289 * @param <X> The extension type. 290 * @param type The extension's class, used as the lookup key. Must not be <jk>null</jk>. 291 * @return The extension, or {@link Optional#empty()} if none was attached under that type. 292 */ 293 public <X> Optional<X> extension(Class<X> type) { 294 reqnn("type", type); 295 return Optional.ofNullable(type.cast(extensions.get(type))); 296 } 297 298 @Override /* Object */ 299 public boolean equals(Object o) { 300 if (this == o) 301 return true; 302 if (! (o instanceof SearchOperator other)) 303 return false; 304 return eq(name, other.name) && eq(help, other.help) && minArgs == other.minArgs 305 && maxArgs == other.maxArgs && eq(types, other.types) && eq(predicate, other.predicate) 306 && custom == other.custom && combinator == other.combinator && typedArgs == other.typedArgs && eq(extensions, other.extensions); 307 } 308 309 @Override /* Object */ 310 public int hashCode() { 311 return h(name, help, minArgs, maxArgs, types, predicate, custom, combinator, typedArgs, extensions); 312 } 313 314 @Override /* Object */ 315 public String toString() { 316 return name; 317 } 318 319 /** 320 * Builder for {@link SearchOperator}. See the class Javadoc for an example. Start one with 321 * {@link SearchOperator#create(String, String)} or {@link SearchOperator#copy()}. 322 */ 323 public static final class Builder { 324 325 private String name; 326 private String help; 327 private int minArgs = 1; 328 private int maxArgs = -1; 329 private Set<SearchType> types = new LinkedHashSet<>(); 330 private SearchPredicate predicate; 331 private boolean custom; 332 private boolean combinator; 333 private boolean typedArgs; 334 private Map<Class<?>,Object> extensions = new LinkedHashMap<>(); 335 336 // Set only by copy() when the source operator was a built-in (custom == false), to the built-in's original 337 // name. Left null for Builders created via create() (always custom) or the package-private builtin() 338 // factory (constructs a built-in directly; it never "renames" anything). build() compares this against the 339 // builder's current name only when non-null, so renaming a copy of a custom operator stays legal, and 340 // builtin()'s own one-time name assignment is never flagged. 341 private String builtinOriginalName; 342 343 private Builder() {} 344 345 /** 346 * Sets the operator name, including the leading {@code $} (e.g. {@code "$near"}). 347 * 348 * @param value The operator name. 349 * @return This object. 350 */ 351 public Builder name(String value) { 352 name = value; 353 return this; 354 } 355 356 /** 357 * Sets the popup help text. 358 * 359 * @param value The help text. 360 * @return This object. 361 */ 362 public Builder help(String value) { 363 help = value; 364 return this; 365 } 366 367 /** 368 * Sets the minimum argument count (inclusive). 369 * 370 * @param value The minimum number of arguments. 371 * @return This object. 372 */ 373 public Builder minArgs(int value) { 374 minArgs = value; 375 return this; 376 } 377 378 /** 379 * Sets the maximum argument count (inclusive), or {@code -1} for unbounded. 380 * 381 * @param value The maximum number of arguments, or {@code -1} for no upper bound. 382 * @return This object. 383 */ 384 public Builder maxArgs(int value) { 385 maxArgs = value; 386 return this; 387 } 388 389 /** 390 * Sets the value types this operator applies to. 391 * 392 * @param value The applicable types, or none to mean "applies to all types". 393 * @return This object. 394 */ 395 public Builder types(SearchType...value) { 396 types = value == null || value.length == 0 ? new LinkedHashSet<>() : new LinkedHashSet<>(l(value)); 397 return this; 398 } 399 400 /** 401 * Attaches the in-memory evaluator for this (custom) operator. 402 * 403 * @param value The predicate the in-memory context calls for this operator. Can be <jk>null</jk> to leave it 404 * with no in-memory evaluator. 405 * @return This object. 406 */ 407 public Builder predicate(SearchPredicate value) { 408 predicate = value; 409 return this; 410 } 411 412 /** 413 * Opts this (custom) operator's SQL renderer into typed argument values. 414 * 415 * <p> 416 * Built-in value operators ({@code $eq}/{@code $ne}/{@code $in}/{@code $gt}/{@code $gte}/{@code $lt}/ 417 * {@code $lte}/{@code $between}) always receive typed arguments from {@link QueryResolver}. For a 418 * <b>custom</b> operator, this flag controls only its SQL renderer: when <jk>true</jk>, a SQL context passes 419 * {@link SearchExpression#typedArgs()} instead of {@link SearchExpression#literalArgs()}. The in-memory 420 * {@link SearchPredicate} always receives literal strings regardless of this flag. 421 * 422 * @param value <jk>true</jk> for the SQL renderer to receive typed arguments. 423 * @return This object. 424 */ 425 public Builder typedArgs(boolean value) { 426 typedArgs = value; 427 return this; 428 } 429 430 /** 431 * Attaches a typed extension, keyed by its own {@link Class}. 432 * 433 * <p> 434 * This is how a downstream module (for example {@code juneau-beanquery-sql}'s SQL renderer) attaches its own 435 * metadata to a custom operator without this module (commons) ever depending on that module's types (design 436 * §5.3). Retrieve it later with {@link SearchOperator#extension(Class)}. 437 * 438 * @param <X> The extension type. 439 * @param type The extension's class, used as the lookup key. Must not be <jk>null</jk>. 440 * @param value The extension value. Can be <jk>null</jk>. 441 * @return This object. 442 */ 443 public <X> Builder extension(Class<X> type, X value) { 444 reqnn("type", type); 445 extensions.put(type, value); 446 return this; 447 } 448 449 /** 450 * Builds the immutable {@link SearchOperator}. 451 * 452 * @return A new, immutable operator. 453 * @throws IllegalArgumentException If {@code name} is <jk>null</jk> or blank, {@code help} is <jk>null</jk> or 454 * blank on a custom operator, {@code minArgs} is negative, {@code maxArgs} is neither {@code -1} nor {@code >= minArgs}, or this 455 * builder was seeded from {@link SearchOperator#copy() copy()} of a built-in operator and {@code name} has 456 * been changed since. 457 */ 458 public SearchOperator build() { 459 reqnb("name", name); 460 if (custom) 461 reqnb("help", help); 462 req(minArgs >= 0, "SearchOperator '%s' minArgs must not be negative: %s", name, minArgs); 463 req(maxArgs == -1 || maxArgs >= minArgs, 464 "SearchOperator '%s' maxArgs (%s) must be -1 (unbounded) or >= minArgs (%s).", name, maxArgs, minArgs); 465 if (builtinOriginalName != null && neq(builtinOriginalName, name)) 466 throw iaex("Built-in operator '%s' cannot be renamed; only help text may be changed on a built-in copy.", builtinOriginalName); 467 return new SearchOperator(this); 468 } 469 } 470}