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.console.views; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022 023import org.apache.juneau.commons.beanquery.*; 024import org.apache.juneau.marshall.collections.*; 025 026/** 027 * One datatable column's search configuration — its value {@link SearchType} and the optional 028 * per-column {@link SearchOperatorSet}. 029 * 030 * <h5 class='section'>Operator gating</h5> 031 * <p> 032 * A column's offerable operators are modeled by a single {@link SearchOperatorSet}, never a string allow-list or a 033 * separate custom-operator map. A custom operator is simply a {@link SearchOperator} placed inside a set via 034 * {@link SearchOperatorSet#standard()}, {@link SearchOperatorSet#with(SearchOperator)}, or 035 * {@link SearchOperatorSet#of(SearchOperator...)}. 036 * <ul> 037 * <li><b>Set absent</b> (default): the column offers the entire type-applicable {@link SearchOperatorSet#standard() 038 * standard} universe (or, when resolved against a table, the table-level set — see {@link ViewDef}). 039 * <li><b>Set present</b> ({@link #operators(SearchOperatorSet)}): the column's set entirely <b>replaces</b> the 040 * table-level set for this column; the effective operators are that set's 041 * {@link SearchOperatorSet#forType(SearchType) type-applicable} members. 042 * </ul> 043 * <p> 044 * An operator not on the effective set is a <b>reject</b> (the popup marks it invalid), never a silent rewrite 045 * — see {@link #offers(String)}. 046 * 047 * <h5 class='section'>Mutability</h5> 048 * <p> 049 * Deliberately mutable, like {@link ViewDef} and {@link RegionDef}: a {@link Column} is a server-side authoring 050 * bean built up once via fluent setters while constructing a view definition, not an 051 * immutable value object. Callers must not mutate one concurrently from more than one thread, and must not reuse 052 * one across requests unless every request treats it as read-only after construction. 053 * 054 * <h5 class='section'>Example:</h5> 055 * <p class='bjava'> 056 * <jc>// A searchable "status" column offering only $eq and $in, plus a custom "$mine" operator.</jc> 057 * Column <jv>status</jv> = Column.<jsm>create</jsm>(<js>"status"</js>) 058 * .label(<js>"Status"</js>) 059 * .searchType(SearchType.<jsf>ID</jsf>) 060 * .operators(SearchOperatorSet.<jsm>of</jsm>( 061 * SearchOperatorSet.<jsm>standard</jsm>().get(<js>"$eq"</js>), 062 * SearchOperatorSet.<jsm>standard</jsm>().get(<js>"$in"</js>), 063 * SearchOperator.<jsm>create</jsm>(<js>"$mine"</js>, <js>"Assigned to me"</js>).types(SearchType.<jsf>ID</jsf>).build())); 064 * </p> 065 * 066 * <h5 class='section'>See Also:</h5> 067 * <ul> 068 * <li class='jc'>{@link SearchOperator} 069 * <li class='jc'>{@link SearchOperatorSet} 070 * <li class='jc'>{@link ViewDef} 071 * </ul> 072 * 073 * @since 10.0.0 074 */ 075@SuppressWarnings({ 076 "java:S1168", // null (not an empty map) is the wire contract: a non-searchable column omits the search block; an empty map would read as searchable. 077 "java:S3776" // Validation and rendering chains many independent guards; splitting them adds no clarity. 078}) 079public final class Column { 080 081 private final String name; 082 private String label; 083 private SearchType searchType; 084 private SearchOperatorSet operators; // null means "inherit the table-level set". 085 private Render render; 086 private String href; 087 private String className; 088 private boolean defaultVisible = true; 089 090 private Column(String name) { 091 this.name = name; 092 } 093 094 /** 095 * Starts a new column definition. 096 * 097 * @param name The column name (matches the row-data key). Must not be 098 * <jk>null</jk> or blank. 099 * @return A new column, not searchable until a {@link #searchType(SearchType)} is set. 100 * @throws IllegalArgumentException If {@code name} is <jk>null</jk> or blank. 101 */ 102 public static Column create(String name) { 103 req(inb(name), "Column name must not be null or blank."); 104 return new Column(name); 105 } 106 107 /** 108 * Sets the human-readable column label. 109 * 110 * @param value The label. Can be <jk>null</jk>. 111 * @return This object. 112 */ 113 public Column label(String value) { 114 label = value; 115 return this; 116 } 117 118 /** 119 * Sets the column's value type, which governs both leaf semantics and the offerable built-in operators. A 120 * column with no search type is not searchable. 121 * 122 * @param value The value type. Can be <jk>null</jk> to mark the column non-searchable. 123 * @return This object. 124 */ 125 public Column searchType(SearchType value) { 126 searchType = value; 127 return this; 128 } 129 130 /** 131 * Sets this column's own {@link SearchOperatorSet} — it entirely replaces the table-level 132 * set for this column. 133 * 134 * @param value The operator set. A <jk>null</jk> value clears it back to <i>absent</i> (inherit the table-level 135 * set, or {@link SearchOperatorSet#standard()} when there is none). 136 * @return This object. 137 */ 138 public Column operators(SearchOperatorSet value) { 139 operators = value; 140 return this; 141 } 142 143 /** 144 * Sets this column's cell renderer from the compact {@code "id:field"} string sugar. 145 * 146 * @param spec The render-id string, e.g. {@code "tag:status"}. Must not be <jk>null</jk> or blank. 147 * @return This object. 148 */ 149 public Column render(String spec) { 150 render = Render.parse(spec); 151 return this; 152 } 153 154 /** 155 * Sets this column's cell renderer. 156 * 157 * @param value The renderer. Can be <jk>null</jk> to unset. 158 * @return This object. 159 */ 160 public Column render(Render value) { 161 render = value; 162 return this; 163 } 164 165 /** 166 * Sets the row-data interpolation template this column's rendered cell links to. 167 * 168 * <p> 169 * An {@code href} with no {@link #render(String) render} implies {@code render("linked")}; the catalog entry 170 * ({@link #toCatalogMap()}) carries that render. An explicit render always wins, whichever order the two are set in. 171 * 172 * @param template The href template, e.g. {@code "{incidentUrl}"}. Can be <jk>null</jk> to unset. 173 * @return This object. 174 */ 175 public Column href(String template) { 176 href = template; 177 return this; 178 } 179 180 /** 181 * Sets a CSS class applied to this column's cells. 182 * 183 * @param css The class name(s). Can be <jk>null</jk> to unset. 184 * @return This object. 185 */ 186 public Column className(String css) { 187 className = css; 188 return this; 189 } 190 191 /** 192 * Sets whether this column starts visible. 193 * 194 * <p> 195 * Defaults to <jk>true</jk>; only {@code false} is ever emitted by {@link #toCatalogMap()}. 196 * 197 * <p> 198 * A {@code false} value only hides the column when the client's {@code columnConfig} (juneau-config.js) is on; without it 199 * the column stays visible. 200 * 201 * @param v Whether the column starts visible. 202 * @return This object. 203 */ 204 public Column defaultVisible(boolean v) { 205 defaultVisible = v; 206 return this; 207 } 208 209 /** 210 * Builds this column's catalog entry: {@code key} and {@code label} plus the display ({@code render}, 211 * {@code href}, {@code className}, {@code defaultVisible}) and search ({@code searchType}, 212 * {@code searchOperators}, {@code customOperators}) metadata, flattened onto one entry, in the form the 213 * {@code datatables} card type resolves. 214 * 215 * <p> 216 * {@code searchOperators} is a list of operator <i>names</i> and {@code customOperators} a list of 217 * {@code {name,help}} maps for the custom ones. Both are emitted only when this column has its own 218 * {@link #operators(SearchOperatorSet) operator set}; a column inheriting the table-level set emits just 219 * {@code searchType}, and a column with no search type emits neither. See 220 * {@link #toCatalogMap(SearchOperatorSet)} to resolve the table-level set into the entry. 221 * 222 * <p> 223 * {@code label} falls back to {@link #name()} when unset. A column with an {@code href} and no {@code render} 224 * emits {@code render} as the {@code linked} renderer. 225 * 226 * @return The catalog entry. 227 */ 228 public JsonMap toCatalogMap() { 229 return toCatalogMap(null); 230 } 231 232 /** 233 * Builds this column's catalog entry like {@link #toCatalogMap()}, but emits the column's <i>effective</i> 234 * operator set: its own set if it has one, else {@code tableDefault}. 235 * 236 * <p> 237 * This is how a table-level set reaches the {@code datatables} card type, which reads operators per column. 238 * With no column set and a <jk>null</jk> {@code tableDefault}, only {@code searchType} is emitted. 239 * 240 * @param tableDefault The table-level operator set inherited by a column with none of its own. Can be 241 * <jk>null</jk>. 242 * @return The catalog entry. 243 */ 244 public JsonMap toCatalogMap(SearchOperatorSet tableDefault) { 245 var m = new JsonMap(); 246 m.put("key", name); 247 m.put("label", label != null ? label : name); 248 if (render != null) 249 m.put("render", render); 250 else if (href != null) 251 m.put("render", Render.of("linked")); 252 if (href != null) 253 m.put("href", href); 254 if (className != null) 255 m.put("className", className); 256 if (! defaultVisible) 257 m.put("defaultVisible", false); 258 if (searchType != null) { 259 m.put("searchType", searchType.wire()); 260 var effective = operators != null ? operators : tableDefault; 261 if (effective != null) { 262 var names = new ArrayList<String>(); 263 var customs = new ArrayList<Map<String,String>>(); 264 for (var op : effective.forType(searchType)) { 265 names.add(op.name()); 266 if (op.isCustom()) { 267 var cm = new LinkedHashMap<String,String>(); 268 cm.put("name", op.name()); 269 cm.put("help", op.help()); 270 customs.add(cm); 271 } 272 } 273 m.put("searchOperators", names); 274 if (! customs.isEmpty()) 275 m.put("customOperators", customs); 276 } 277 } 278 return m; 279 } 280 281 /** @return The column name. */ 282 public String name() { 283 return name; 284 } 285 286 /** @return The column label, or <jk>null</jk>. */ 287 public String label() { 288 return label; 289 } 290 291 /** @return The column value type, or <jk>null</jk> if the column is not searchable. */ 292 public SearchType searchType() { 293 return searchType; 294 } 295 296 /** 297 * This column's own operator set, or <jk>null</jk> if absent (inherit the table-level set). 298 * 299 * @return The operator set, or <jk>null</jk>. 300 */ 301 public SearchOperatorSet operators() { 302 return operators; 303 } 304 305 /** @return <jk>true</jk> if this column has a search type (and is therefore searchable). */ 306 public boolean searchable() { 307 return searchType != null; 308 } 309 310 /** 311 * Resolves the effective operator set the header popup offers for this column, against the 312 * built-in {@link SearchOperatorSet#standard() standard} set as the fallback default. 313 * 314 * @return The effective operators, in the resolved set's order; empty when the column is not searchable. 315 */ 316 public List<SearchOperator> effectiveOperators() { 317 return effectiveOperators(null); 318 } 319 320 /** 321 * Resolves the effective operator set the header popup offers for this column, inheriting the 322 * specified table-level set when this column has none of its own. 323 * 324 * @param tableDefault The table-level operator set to inherit when this column has none of its own. Can be 325 * <jk>null</jk>, in which case {@link SearchOperatorSet#standard()} is used. 326 * @return The effective operators, in the resolved set's order; empty when the column is not searchable. 327 */ 328 public List<SearchOperator> effectiveOperators(SearchOperatorSet tableDefault) { 329 if (searchType == null) 330 return List.of(); 331 var inherited = tableDefault != null ? tableDefault : SearchOperatorSet.standard(); 332 var set = operators != null ? operators : inherited; 333 return set.forType(searchType); 334 } 335 336 /** 337 * Whether the named operator is on this column's effective set — i.e. whether the popup offers it (and 338 * accepts it rather than rejecting it as invalid). 339 * 340 * @param opName The operator name, including the leading {@code $}. Can be <jk>null</jk>. 341 * @return <jk>true</jk> if the operator is offered for this column. 342 */ 343 public boolean offers(String opName) { 344 if (opName == null) 345 return false; 346 for (var op : effectiveOperators()) 347 if (op.name().equals(opName)) 348 return true; 349 return false; 350 } 351 352 /** 353 * Builds the per-column {@code search} block for the {@code VIEW_META} wire contract 354 * directly from author-catalog inputs, without the caller having to touch the search-engine beans. 355 * 356 * <p> 357 * This is the entry point the chrome/authoring layer uses: it takes only the wire token, the optional 358 * allowed-operator names, and the custom operators as plain {@code {name,help}} maps — so a chrome-only 359 * module that must not depend on the search engine can still emit a column's search block by calling here. 360 * Internally this builds a single {@link SearchOperatorSet}: an allow-list becomes 361 * {@link SearchOperatorSet#of(SearchOperator...)} with exactly the named operators (customs preferred over 362 * same-named built-ins), while an absent allow-list becomes {@link SearchOperatorSet#standard()} with each 363 * custom appended via {@link SearchOperatorSet#with(SearchOperator)}. 364 * 365 * @param name The column name (row-data key). Must not be <jk>null</jk> or blank. 366 * @param searchTypeWire The wire {@link SearchType} token, or <jk>null</jk>. 367 * @param operatorAllowList The explicit allowed-operator names, or <jk>null</jk> for the default 368 * (the full type-applicable universe). 369 * @param customOperators Per-column custom operators as {@code {name,help}} maps; may be 370 * <jk>null</jk> or empty. 371 * @return The {@code {type, operators:[...]}} block, or <jk>null</jk> when {@code searchTypeWire} names no known 372 * type (a non-searchable column emits no search block). 373 */ 374 @SuppressWarnings({ 375 "java:S3776" // Allow-list vs. default branches with custom-operator merging read best inline. 376 }) 377 public static JsonMap searchMeta(String name, String searchTypeWire, List<String> operatorAllowList, List<Map<String,String>> customOperators) { 378 var type = SearchType.fromWire(searchTypeWire); 379 if (type == null) 380 return null; 381 var customs = new LinkedHashMap<String,SearchOperator>(); 382 if (customOperators != null) 383 for (var cm : customOperators) 384 customs.put(cm.get("name"), SearchOperator.create(cm.get("name"), cm.get("help")).types(type).build()); 385 SearchOperatorSet set; 386 if (operatorAllowList != null) { 387 var ops = new ArrayList<SearchOperator>(); 388 for (var n : operatorAllowList) { 389 var op = customs.get(n); 390 if (op == null) 391 op = SearchOperatorSet.standard().get(n); 392 if (op != null) 393 ops.add(op); 394 } 395 set = SearchOperatorSet.of(ops.toArray(new SearchOperator[0])); 396 } else { 397 set = SearchOperatorSet.standard(); 398 for (var op : customs.values()) 399 set = set.with(op); 400 } 401 return create(name).searchType(type).operators(set).searchMeta(); 402 } 403 404 /** 405 * Serializes this column's effective search configuration to the {@code VIEW_META} {@code search} block: the wire 406 * {@link SearchType} token plus one entry per 407 * {@link #effectiveOperators() effective operator}, each carrying the metadata the header popup renders. 408 * 409 * <p> 410 * <c>help</c> is present only for custom operators; the popup supplies built-in help text from <c>juneau-search.js</c>. 411 * 412 * @return The {@code {type, operators:[{name,help,minArgs,maxArgs,combinator,custom}]}} block, or <jk>null</jk> 413 * if this column is not searchable. 414 */ 415 public JsonMap searchMeta() { 416 return searchMeta(null); 417 } 418 419 /** 420 * Serializes this column's effective search configuration to the {@code VIEW_META} {@code search} block, inheriting 421 * the specified table-level operator set when this column has none of its 422 * own. 423 * 424 * @param tableDefault The table-level operator set to inherit when this column has none of its own. Can be 425 * <jk>null</jk>, in which case {@link SearchOperatorSet#standard()} is used. 426 * @return The {@code {type, operators:[{name,help,minArgs,maxArgs,combinator,custom}]}} block, or <jk>null</jk> 427 * if this column is not searchable. 428 */ 429 public JsonMap searchMeta(SearchOperatorSet tableDefault) { 430 if (searchType == null) 431 return null; 432 var operatorList = new JsonList(); 433 for (var op : effectiveOperators(tableDefault)) { 434 var o = new JsonMap(); 435 o.put("name", op.name()); 436 if (op.help() != null) 437 o.put("help", op.help()); 438 o.put("minArgs", op.minArgs()); 439 o.put("maxArgs", op.maxArgs()); 440 o.put("combinator", op.isCombinator()); 441 o.put("custom", op.isCustom()); 442 operatorList.add(o); 443 } 444 var search = new JsonMap(); 445 search.put("type", searchType.wire()); 446 search.put("operators", operatorList); 447 return search; 448 } 449}