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}