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 &mdash; 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 * &mdash; 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} &mdash; 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 &mdash; 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 &mdash; 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}