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.beanquery.SearchType.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.util.*;
023
024/**
025 * An immutable, ordered collection of {@link SearchOperator}s that governs which {@code $}-operators the parser
026 * recognizes.
027 *
028 * <p>
029 * {@link #standard()} is the built-in set (the confirmed operators with their arity and per-type applicability).
030 * Applications extend it with {@link #with(SearchOperator)} (add a custom operator to a copy) or build a bespoke set
031 * with {@link #of(SearchOperator...)}. A context uses one set as its default and may hold a per-column map; a
032 * per-column set <b>replaces</b> the default for that column rather than merging with it.
033 *
034 * <p>
035 * Juneau uses the spellings {@code $prefix} and {@code $blank} as first-class names; there are deliberately <b>no</b>
036 * {@code $starts}/{@code $empty} aliases.
037 *
038 * <h5 class='section'>Case-insensitive names</h5>
039 * <p>
040 * Lookup ({@link #get(String)}, {@link #contains(String)}, {@link #without(String)}) is case-insensitive: {@code
041 * $EQ}, {@code $Eq} and {@code $eq} all resolve to the same operator. {@link #of(SearchOperator...)} rejects two
042 * operators whose names differ only in case; {@link #with(SearchOperator)} replaces a same-name-ignoring-case
043 * operator, the same way it replaces an exact-name one. {@link #operators()} and {@link #get(String)} always expose
044 * the operator's own canonical (registered) name, never the casing a caller looked it up with.
045 *
046 * <h5 class='section'>Example:</h5>
047 * <p class='bjava'>
048 *    <jc>// Hide $regex unless a caller opts in.</jc>
049 *    SearchOperatorSet <jv>restricted</jv> = SearchOperatorSet.<jsm>standard</jsm>().without(<js>"$regex"</js>);
050 *
051 *    <jc>// $EQ resolves to the same operator as $eq.</jc>
052 *    SearchOperator <jv>eq</jv> = <jv>restricted</jv>.get(<js>"$EQ"</js>);
053 * </p>
054 *
055 * @since 10.0.0
056 */
057public final class SearchOperatorSet {
058
059   private static final SearchOperatorSet STANDARD;
060   static {
061      var ops = new SearchOperator[] {
062         SearchOperator.builtin("$eq", 1, 1, false, TEXT, ID, NUMERIC, VERSION, TIMESTAMP, ENUM, BOOLEAN),
063         SearchOperator.builtin("$eqic", 1, 1, false, TEXT, ID, ENUM),
064         SearchOperator.builtin("$ne", 1, -1, false, TEXT, ID, NUMERIC, VERSION, TIMESTAMP, ENUM, BOOLEAN),
065         SearchOperator.builtin("$in", 1, -1, false, TEXT, ID, NUMERIC, VERSION, TIMESTAMP, ENUM),
066         SearchOperator.builtin("$and", 2, -1, true),
067         SearchOperator.builtin("$or", 2, -1, true),
068         SearchOperator.builtin("$not", 1, 1, true),
069         SearchOperator.builtin("$contains", 1, 1, false, TEXT, ID),
070         SearchOperator.builtin("$prefix", 1, 1, false, TEXT, ID, VERSION),
071         SearchOperator.builtin("$regex", 1, 2, false, TEXT, ID),
072         SearchOperator.builtin("$blank", 0, 0, false, TEXT, ID),
073         SearchOperator.builtin("$gt", 1, 1, false, NUMERIC, VERSION, TIMESTAMP),
074         SearchOperator.builtin("$gte", 1, 1, false, NUMERIC, VERSION, TIMESTAMP),
075         SearchOperator.builtin("$lt", 1, 1, false, NUMERIC, VERSION, TIMESTAMP),
076         SearchOperator.builtin("$lte", 1, 1, false, NUMERIC, VERSION, TIMESTAMP),
077         SearchOperator.builtin("$between", 2, 2, false, NUMERIC, VERSION, TIMESTAMP),
078      };
079      var m = new LinkedHashMap<String,SearchOperator>();
080      for (var op : ops)
081         put(m, op);
082      STANDARD = new SearchOperatorSet(m);
083   }
084
085   private final Map<String,SearchOperator> operators;
086
087   private SearchOperatorSet(Map<String,SearchOperator> operators) {
088      this.operators = Collections.unmodifiableMap(new LinkedHashMap<>(operators));
089   }
090
091   /** Lower-cases a name for use as a map key (D6: lookup is case-insensitive; {@code Locale.ROOT}). */
092   private static String key(String name) {
093      return name.toLowerCase(Locale.ROOT);
094   }
095
096   /**
097    * The built-in operator set.
098    *
099    * @return The standard set, never <jk>null</jk>.
100    */
101   public static SearchOperatorSet standard() {
102      return STANDARD;
103   }
104
105   /**
106    * Builds a set from exactly the specified operators (no built-ins are implied).
107    *
108    * @param operators The operators, in the order to expose them. Must not be <jk>null</jk>; names must be unique
109    *    ignoring case.
110    * @return A new set.
111    */
112   public static SearchOperatorSet of(SearchOperator...operators) {
113      req(operators != null, "SearchOperatorSet.of operators must not be null.");
114      var m = new LinkedHashMap<String,SearchOperator>();
115      for (var op : operators)
116         put(m, op);
117      return new SearchOperatorSet(m);
118   }
119
120   /**
121    * Returns a copy of this set with the specified operator added, replacing a same-name-ignoring-case operator if
122    * one is already present.
123    *
124    * @param op The operator to add. Must not be <jk>null</jk>.
125    * @return A new set; this set is unchanged.
126    */
127   public SearchOperatorSet with(SearchOperator op) {
128      req(op != null, "SearchOperatorSet.with operator must not be null.");
129      var m = new LinkedHashMap<>(operators);
130      m.put(key(op.name()), op);
131      return new SearchOperatorSet(m);
132   }
133
134   /**
135    * Returns a copy of this set without the named operator.
136    *
137    * @param name The operator name (for example {@code "$regex"}), matched ignoring case. Can be <jk>null</jk>.
138    * @return A new set, or this set if it has no operator with that name.
139    */
140   public SearchOperatorSet without(String name) {
141      if (! contains(name))
142         return this;
143      var m = new LinkedHashMap<>(operators);
144      m.remove(key(name));
145      return new SearchOperatorSet(m);
146   }
147
148   private static void put(Map<String,SearchOperator> m, SearchOperator op) {
149      req(op != null, "SearchOperatorSet operator must not be null.");
150      var key = key(op.name());
151      var existing = m.get(key);
152      if (existing != null) {
153         if (eq(existing.name(), op.name()))
154            req(false, "SearchOperatorSet contains a duplicate operator name: '%s'", op.name());
155         req(false, "SearchOperatorSet contains operators whose names differ only in case: '%s' and '%s'.", existing.name(), op.name());
156      }
157      m.put(key, op);
158   }
159
160   /**
161    * Looks up an operator by name, ignoring case.
162    *
163    * @param name The operator name, including the leading {@code $} (e.g. {@code "$eq"} or {@code "$EQ"}). Can be
164    *    <jk>null</jk>.
165    * @return The operator (with its canonical, registered-case name), or <jk>null</jk> if not in this set.
166    */
167   public SearchOperator get(String name) {
168      return name == null ? null : operators.get(key(name));
169   }
170
171   /**
172    * Whether this set contains an operator with the specified name, ignoring case.
173    *
174    * @param name The operator name. Can be <jk>null</jk>.
175    * @return <jk>true</jk> if present.
176    */
177   public boolean contains(String name) {
178      return name != null && operators.containsKey(key(name));
179   }
180
181   /**
182    * All operators in this set, in canonical order.
183    *
184    * @return An unmodifiable list.
185    */
186   public List<SearchOperator> operators() {
187      return List.copyOf(operators.values());
188   }
189
190   /**
191    * The operators in this set offered for the specified value type, including the combinators (which apply to every
192    * type).
193    *
194    * @param type The column value type. Can be <jk>null</jk> (yields an empty list).
195    * @return An unmodifiable list of applicable operators, in canonical order.
196    */
197   public List<SearchOperator> forType(SearchType type) {
198      var l = new ArrayList<SearchOperator>();
199      if (type != null)
200         for (var op : operators.values())
201            if (op.appliesTo(type))
202               l.add(op);
203      return List.copyOf(l);
204   }
205}