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.http.*;
024import org.apache.juneau.terminal.report.TerminalViewChecks;
025
026/**
027 * A page "region": a named container whose contents a client-side populator fills in, chosen by name via
028 * {@link #populate} rather than declared inline.
029 *
030 * <p>
031 * Beyond the identity/populator core, this bean also carries the <b>declarative descriptor</b>: a data source ({@link
032 * #dataUrl} / {@link #params}), an inline renderer ({@link #renderer}),
033 * lazy activation ({@link #lazy}), a refresh interval ({@link #refreshMs}), and a field/title projection
034 * ({@link #fields} / {@link #titleFields}). A descriptor that omits {@link #populate} resolves to the
035 * Juneau-shipped declarative default ({@code JuneauViews.regions.defaultPopulate}), which reads exactly
036 * this descriptor (via {@code ctx.declared}) and nothing else.
037 *
038 * <h5 class='section'>Mutability</h5>
039 * <p>
040 * Deliberately mutable, like {@link Column} and {@link ViewDef} (the same rule, stated here since this class
041 * predates it): a {@link RegionDef} is a server-side authoring bean built up once via its public fields and
042 * fluent setters while constructing a page, not an immutable value object.  Callers must not
043 * mutate one concurrently from more than one thread, and must not reuse one across requests unless every request
044 * treats it as read-only after construction.
045 *
046 * <h5 class='section'>Example:</h5>
047 * <p class='bjava'>
048 *    <jc>// A lazily-activated row-detail region backed by a data URL, refreshing every 30 seconds.</jc>
049 *    RegionDef <jv>region</jv> = RegionDef.<jsm>create</jsm>(<js>"release-notes"</js>)
050 *       .type(RegionDef.<jsf>TYPE_ROW_DETAIL</jsf>)
051 *       .dataUrl(<js>"/api/releases/{id}/notes"</js>)
052 *       .lazy(<jk>true</jk>)
053 *       .refreshMs(30_000L);
054 * </p>
055 *
056 * @since 10.0.0
057 */
058@SuppressWarnings({
059   "java:S1845", // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
060   "java:S3776" // validate*() and toContractMap() (also in Field) chain many independent null/blank guards; each is trivial, so splitting adds no clarity
061})
062public class RegionDef {
063
064   /**
065    * The frozen contract version for the region envelope.
066    *
067    * <p>
068    * Deliberately independent of {@link ViewsMixin#CONTRACT_VERSION}: a region-contract revision must never
069    * force a view-sidecar bump, or vice-versa.
070    */
071   public static final String CONTRACT_VERSION = "1";
072
073   /** Wire token for a row-detail region. */
074   public static final String TYPE_ROW_DETAIL = "row-detail";
075
076   /** Wire token for a card-body region. */
077   public static final String TYPE_CARD_BODY = "card-body";
078
079   /** Wire token for a tab-body region. */
080   public static final String TYPE_TAB_BODY = "tab-body";
081
082   private static final Set<String> TYPES = st(TYPE_ROW_DETAIL, TYPE_CARD_BODY, TYPE_TAB_BODY);
083
084   /** The reserved renderer name that requires a non-empty {@link #fields} catalog. */
085   private static final String RENDERER_FIELD_GRID = "field-grid";
086
087   /**
088    * The minimum honored {@link #refreshMs}, in milliseconds.
089    *
090    * <p>
091    * A declared interval below this floor is clamped up to it rather than honored as configured &mdash; the same
092    * reasoning and the same shared floor as {@link SafePathTemplate#MIN_POLL_INTERVAL_MS}.
093    */
094   public static final long MIN_REFRESH_MS = SafePathTemplate.MIN_POLL_INTERVAL_MS;
095
096   /** This region's own id, unique within its host. */
097   public String id;
098
099   /**
100    * The name of the client populator to resolve via {@code JuneauViews.regions.resolve(name)}, or <jk>null</jk>
101    * for the reserved declarative default (<js>"default"</js>, see {@link PopulatorAllowlist#BUILTIN_IDS}).
102    *
103    * <p>
104    * This is a name looked up in the client registry rather than an inline function for two reasons: an inline
105    * function cannot survive the JSON sidecar this envelope travels in, and a name &mdash; unlike an arbitrary
106    * function body &mdash; is the one shape a server-side allowlist ({@link PopulatorAllowlist}) can meaningfully
107    * constrain at all.
108    */
109   public String populate;
110
111   /**
112    * This region's type: {@link #TYPE_ROW_DETAIL}, {@link #TYPE_CARD_BODY}, or {@link #TYPE_TAB_BODY}.
113    *
114    * <p>
115    * Set by the enclosing host (a detail panel, a card, or a tab body) rather than by the region's own author in
116    * the common case; exposed here (rather than only in the sidecar) because {@link #validate()} needs it to
117    * enforce the type-scoped field-catalog rules &mdash; a {@link #TYPE_ROW_DETAIL} region must
118    * carry no {@link #fields}, and a {@link #TYPE_CARD_BODY}/{@link #TYPE_TAB_BODY} region using the
119    * {@code "field-grid"} {@link #renderer} under the reserved default must carry at least one.
120    */
121   public String type;
122
123   /**
124    * A same-origin path from which the declarative default (and {@code ctx.fetchDeclared()}) fetches this
125    * region's payload. <jk>null</jk> means "no declared fetch" &mdash; legal, and the normal state for a custom
126    * populator that fetches its own way or paints with no fetch at all.
127    *
128    * <p>
129    * Validated with {@link #isSafeDetailEndpoint(String)} (same-origin, no scheme, no leading {@code //},
130    * no {@code ..} segment).
131    */
132   public String dataUrl;
133
134   /**
135    * Author-declared request parameters, opaque to the framework as a <b>payload</b> (nothing routes or branches
136    * on their contents) but not opaque to the query-string serializer that appends them to {@link #dataUrl}. Use a
137    * {@link LinkedHashMap} (or an equivalent ordered map) when key order matters to
138    * the endpoint: the serialization walks {@link Map#entrySet()} in the map's own iteration order.
139    */
140   public Map<String,Object> params;
141
142   /**
143    * The name of the {@code JuneauViews.helpers} function the declarative default paints with,
144    * e.g. <js>"field-grid"</js>. <jk>null</jk> when a custom {@link #populate} does its own painting.
145    */
146   public String renderer;
147
148   /**
149    * Whether this region's first populate is deferred until it becomes visible.
150    *
151    * <p>
152    * <jk>null</jk> means "use the per-region-type default": <jk>false</jk> for {@link #TYPE_ROW_DETAIL} and
153    * {@link #TYPE_CARD_BODY}, <jk>true</jk> for {@link #TYPE_TAB_BODY} &mdash; see {@link #effectiveLazy()}.
154    */
155   public Boolean lazy;
156
157   /**
158    * The declared auto-refresh interval, in milliseconds, or <jk>null</jk> for no polling.
159    *
160    * <p>
161    * A value below {@link #MIN_REFRESH_MS} is silently clamped up to the floor rather than rejected.
162    */
163   public Long refreshMs;
164
165   /**
166    * The field catalog: labels, renderers, hrefs, spans and per-field action-bar ids, declared once per region
167    * rather than repeated per row/refresh. Projects onto {@code ctx.declared.fields}.
168    *
169    * <p>
170    * <b>Card and tab regions only.</b> A {@link #TYPE_ROW_DETAIL} region's catalog is an author-declared JS
171    * literal handed straight to {@code fieldGrid} client-side; declaring {@link #fields} on a
172    * {@link #TYPE_ROW_DETAIL} region fails {@link #validate()}.
173    */
174   public List<Field> fields;
175
176   /**
177    * Chrome's declared title-key allowlist &mdash; the one descriptor field whose only
178    * reader is the framework's own title painter, never the populate (see the
179    * three-way partition; the declarative default's source must not reference this field).
180    */
181   public List<String> titleFields;
182
183   /**
184    * App-approved populator names allowed on {@link #populate} in addition to
185    * {@link PopulatorAllowlist#BUILTIN_IDS}. Blank entries fail {@link #validate()}.
186    *
187    * <p>
188    * Opting a name in here is authoring discipline, not a security boundary &mdash; see
189    * {@link PopulatorAllowlist}'s class Javadoc.
190    */
191   public Set<String> allowedPopulators;
192
193   /**
194    * Creates a region with the given id.
195    *
196    * <p>
197    * The identity plus populator name.  Page slots do not go through this factory &mdash; author HTML plus
198    * {@code JuneauViews.regions.mount({ id: populatorName })}.  Not {@code @Deprecated}.
199    * </p>
200    *
201    * @param id This region's own id, unique within its host. Must not be <jk>null</jk> or blank.
202    * @return A new {@link RegionDef}.
203    */
204   public static RegionDef create(String id) {
205      var r = new RegionDef();
206      r.id = id;
207      return r;
208   }
209
210   /**
211    * Sets the name of the client populator to resolve.
212    *
213    * @param value A populator name, or <jk>null</jk> to use the reserved declarative default.
214    * @return This object.
215    */
216   public RegionDef populate(String value) {
217      populate = value;
218      return this;
219   }
220
221   /**
222    * Sets this region's type.
223    *
224    * @param value One of {@link #TYPE_ROW_DETAIL}, {@link #TYPE_CARD_BODY}, {@link #TYPE_TAB_BODY}.
225    * @return This object.
226    */
227   public RegionDef type(String value) {
228      type = value;
229      return this;
230   }
231
232   /**
233    * Sets the same-origin data-fetch endpoint.
234    *
235    * @param value The path. Must be a same-origin path template (no {@code ://}, no leading {@code //}, no
236    *    scheme colon-before-slash, no {@code ..} segment) &mdash; enforced at {@link #validate()}.
237    * @return This object.
238    */
239   public RegionDef dataUrl(String value) {
240      dataUrl = value;
241      return this;
242   }
243
244   /**
245    * Sets the author-declared request parameters.
246    *
247    * @param value The parameters. A {@link LinkedHashMap} (or equivalent ordered map) when key order matters.
248    * @return This object.
249    */
250   public RegionDef params(Map<String,Object> value) {
251      params = value;
252      return this;
253   }
254
255   /**
256    * Sets the author-declared request parameters from key/value pairs, preserving call order.
257    *
258    * @param keyValuePairs An even-length {@code key, value, key, value, ...} sequence.
259    * @return This object.
260    */
261   public RegionDef params(Object...keyValuePairs) {
262      if (keyValuePairs.length % 2 != 0)
263         throw iaex("RegionDef.params(...) requires an even number of key/value arguments.");
264      var m = new LinkedHashMap<String,Object>();
265      for (var i = 0; i < keyValuePairs.length; i += 2)
266         m.put(String.valueOf(keyValuePairs[i]), keyValuePairs[i + 1]);
267      params = m;
268      return this;
269   }
270
271   /**
272    * Sets the helper name the declarative default paints with.
273    *
274    * @param value A {@code JuneauViews.helpers} function name, e.g. <js>"field-grid"</js>.
275    * @return This object.
276    */
277   public RegionDef renderer(String value) {
278      renderer = value;
279      return this;
280   }
281
282   /**
283    * Sets whether this region's first populate is deferred until it becomes visible.
284    *
285    * @param value <jk>true</jk>/<jk>false</jk>, or leave unset (<jk>null</jk>) for the per-region-type default.
286    * @return This object.
287    */
288   public RegionDef lazy(boolean value) {
289      lazy = value;
290      return this;
291   }
292
293   /**
294    * Declares that this region should be polled (re-fetched and re-populated) on the given interval.
295    *
296    * <p>
297    * A value below {@link #MIN_REFRESH_MS} is silently clamped up to the floor rather than rejected.
298    *
299    * @param intervalMs The desired polling interval, in milliseconds. Must be positive.
300    * @return This object.
301    */
302   public RegionDef refreshMs(long intervalMs) {
303      if (intervalMs <= 0)
304         throw iaex("RegionDef.refreshMs(...) interval must be positive.");
305      refreshMs = SafePathTemplate.clampPollInterval(intervalMs);
306      return this;
307   }
308
309   /**
310    * The rules that must all match for the whole region to render.  <jk>null</jk> or empty means always visible.
311    * See {@link RowAction#visibleWhen} for the shared semantics and evaluation-map convention.
312    */
313   public List<VisibilityRule> visibleWhen;
314
315   /**
316    * Hides this region unless every rule matches.
317    *
318    * @param rules The rules; all must match.  Replaces any rules set by an earlier call.
319    * @return This object.
320    */
321   public RegionDef visibleWhen(VisibilityRule...rules) {
322      visibleWhen = l(rules);
323      return this;
324   }
325
326   /**
327    * Populator-owned structured configuration.  <jk>null</jk> or empty means none.
328    *
329    * <p>
330    * Unlike {@link #params}, nested maps and lists are allowed, and the value is never appended to {@link #dataUrl}
331    * nor sent back to the server: it reaches the client only as {@code ctx.declared.config}.  Its shape belongs to
332    * the populator that reads it (for example {@link JobLogRegion}); {@link #validate()} only checks that it is
333    * JSON-serializable (string keys, no cycles).
334    */
335   public Map<String,Object> config;
336
337   /**
338    * Sets the populator-owned structured configuration.
339    *
340    * @param value The configuration; see {@link #config}.
341    * @return This object.
342    */
343   public RegionDef config(Map<String,Object> value) {
344      config = value;
345      return this;
346   }
347
348   /**
349    * Sets the field catalog.
350    *
351    * @param value The catalog, in declaration order. Card/tab regions only &mdash; see {@link #fields}.
352    * @return This object.
353    */
354   public RegionDef fields(Field...value) {
355      fields = l(value);
356      return this;
357   }
358
359   /**
360    * Sets chrome's declared title-key allowlist.
361    *
362    * @param value The allowlisted keys.
363    * @return This object.
364    */
365   public RegionDef titleFields(String...value) {
366      titleFields = l(value);
367      return this;
368   }
369
370   /**
371    * Opts in app-approved populator names for {@link #populate}, in addition to
372    * {@link PopulatorAllowlist#BUILTIN_IDS}.
373    *
374    * @param value Populator names. Must not contain blank entries.
375    * @return This object.
376    */
377   public RegionDef allowPopulators(String...value) {
378      allowedPopulators = st(value);
379      return this;
380   }
381
382   /**
383    * The effective {@link #lazy} value once the per-region-type default is applied.
384    *
385    * <p>
386    * <jk>false</jk> for {@link #TYPE_ROW_DETAIL} (the one region populates on expand, not lazily) and
387    * {@link #TYPE_CARD_BODY} (a card is visible when its grid is); <jk>true</jk> for {@link #TYPE_TAB_BODY}
388    * (preserving today's init-on-activate). An explicit {@link #lazy} value always wins over the per-type
389    * default, for any {@link #type} including <jk>null</jk>.
390    *
391    * @return The effective lazy flag.
392    */
393   public boolean effectiveLazy() {
394      if (lazy != null)
395         return lazy;
396      return eq(type, TYPE_TAB_BODY);
397   }
398
399   /**
400    * Fail-closed bean validation.
401    *
402    * <p>
403    * Beyond the original id/populator checks, this now also enforces: a same-origin {@link #dataUrl}; a
404    * recognized {@link #type} (when set); no nested-map {@link #params} value and no {@link #params} key
405    * colliding with a key already present in {@link #dataUrl}'s own query string; the
406    * card/tab-only field catalog and its two startup rejections; and
407    * no blank {@link #titleFields} entry.
408    *
409    * @throws IllegalArgumentException If this definition is not well-formed.
410    */
411   public void validate() {
412      if (ib(id))
413         throw iaex("RegionDef id must not be null or blank.");
414      if (allowedPopulators != null)
415         for (var name : allowedPopulators)
416            if (ib(name))
417               throw iaex("allowPopulators entry must not be blank.");
418      if (populate != null)
419         PopulatorAllowlist.assertAllowed(populate, allowedPopulators);
420      if (type != null && !TYPES.contains(type))
421         throw iaex("RegionDef '%s' type must be one of %s, was '%s'.", id, TYPES, type);
422      validateDataUrlAndParams();
423      validateTitleFields();
424      validateFields();
425      validateConfig();
426   }
427
428   /**
429    * Whether {@code endpoint} is a same-origin path template: no {@code ://} , no {@code //} prefix, no scheme
430    * colon-before-slash, and no {@code ..} path segments.
431    *
432    * @param endpoint The candidate template.  May be <jk>null</jk>.
433    * @return <jk>true</jk> if the string is a same-origin path template.
434    */
435   public static boolean isSafeDetailEndpoint(String endpoint) {
436      return TerminalViewChecks.isSameOriginPath(endpoint);
437   }
438
439   private void validateDataUrlAndParams() {
440      if (inb(dataUrl) && !isSafeDetailEndpoint(dataUrl))
441         throw iaex("RegionDef '%s' dataUrl must be a same-origin path (no scheme, no leading '//', no '..' "
442            + "segment): %s", id, dataUrl);
443      if (ie(params))
444         return;
445      for (var e : params.entrySet()) {
446         if (e.getValue() instanceof Map)
447            throw iaex("RegionDef '%s' params entry '%s' must not be a nested map/object; encode it into a "
448               + "string value yourself if you need structure.", id, e.getKey());
449      }
450      var dataUrlKeys = queryKeysOf(dataUrl);
451      for (var key : params.keySet())
452         if (dataUrlKeys.contains(key))
453            throw iaex("RegionDef '%s' params key '%s' collides with a key already present in dataUrl's own "
454               + "query string.", id, key);
455   }
456
457   private void validateTitleFields() {
458      if (titleFields == null)
459         return;
460      for (var f : titleFields)
461         if (ib(f))
462            throw iaex("RegionDef '%s' titleFields entry must not be blank.", id);
463   }
464
465   private void validateFields() {
466      if (eq(type, TYPE_ROW_DETAIL) && fields != null && !fields.isEmpty())
467         throw iaex("RegionDef '%s' is type '%s' and must not declare fields; a row-detail region's catalog "
468            + "is an author JS literal, never a projected one.", id, TYPE_ROW_DETAIL);
469      if (fields != null) {
470         var keys = new HashSet<String>();
471         for (var f : fields) {
472            if (f == null)
473               throw iaex("RegionDef '%s' fields entry must not be null.", id);
474            f.validate(id);
475            if (!keys.add(f.data))
476               throw iaex("RegionDef '%s' duplicate field data key '%s'.", id, f.data);
477         }
478      }
479      var isDefaultPopulate = populate == null || eq(populate, "default");
480      var isCardOrTab = eqa(type, TYPE_CARD_BODY, TYPE_TAB_BODY);
481      if (isDefaultPopulate && isCardOrTab && eq(renderer, RENDERER_FIELD_GRID) && (ie(fields)))
482         throw iaex("RegionDef '%s' declares renderer 'field-grid' under the default populator but no fields; "
483            + "a field-grid with no catalog would silently paint an unlabeled/empty grid.", id);
484   }
485
486   private void validateConfig() {
487      if (config != null)
488         checkSerializable(config, Collections.newSetFromMap(new IdentityHashMap<>()));
489   }
490
491   private void checkSerializable(Object v, Set<Object> path) {
492      if (v instanceof Map<?,?> || v instanceof Collection<?>) {
493         if (! path.add(v))
494            throw iaex("RegionDef '%s' config must not contain a cycle.", id);
495         if (v instanceof Map<?,?> m) {
496            for (var e : m.entrySet()) {
497               if (! (e.getKey() instanceof String))
498                  throw iaex("RegionDef '%s' config keys must be strings.", id);
499               checkSerializable(e.getValue(), path);
500            }
501         } else {
502            for (var x : (Collection<?>)v)
503               checkSerializable(x, path);
504         }
505         path.remove(v);
506         return;
507      }
508      if (v == null || v instanceof String || v instanceof Number || v instanceof Boolean)
509         return;
510      throw iaex("RegionDef '%s' config values must be maps, lists, strings, numbers, booleans or null; got %s.", id,
511         v.getClass().getSimpleName());
512   }
513
514   /** Extracts the query-string keys already present in {@code url}'s own {@code ?...} portion, if any. */
515   private static Set<String> queryKeysOf(String url) {
516      if (url == null)
517         return Set.of();
518      var q = url.indexOf('?');
519      if (q < 0 || q == url.length() - 1)
520         return Set.of();
521      var keys = new HashSet<String>();
522      for (var pair : url.substring(q + 1).split("&")) {
523         if (pair.isEmpty())
524            continue;
525         var eq = pair.indexOf('=');
526         keys.add(eq < 0 ? pair : pair.substring(0, eq));
527      }
528      return keys;
529   }
530
531   /**
532    * Serializes this descriptor to the compact JSON envelope the {@code data-juneau-region-contract} sidecar
533    * attribute carries, hand-built (rather than reflected) so the key set, order, and each
534    * nested {@link Field}'s wire shape are exactly the ones the contract specifies.
535    *
536    * @return An ordered {@link Map} ready for {@code Json.of(...)}.
537    */
538   public Map<String,Object> toContractMap() {
539      var m = new LinkedHashMap<String,Object>();
540      m.put("contractVersion", CONTRACT_VERSION);
541      m.put("id", id);
542      if (type != null)
543         m.put("type", type);
544      if (populate != null)
545         m.put("populate", populate);
546      if (dataUrl != null)
547         m.put("dataUrl", dataUrl);
548      if (params != null && !params.isEmpty())
549         m.put("params", params);
550      if (renderer != null)
551         m.put("renderer", renderer);
552      m.put("lazy", effectiveLazy());
553      if (refreshMs != null)
554         m.put("refreshMs", refreshMs);
555      if (titleFields != null && !titleFields.isEmpty())
556         m.put("titleFields", titleFields);
557      if (fields != null && !fields.isEmpty()) {
558         m.put("fields", fields.stream().map(Field::toContractMap).toList());
559      }
560      if (visibleWhen != null && !visibleWhen.isEmpty())
561         m.put("visibleWhen", VisibilityRule.toMaps(visibleWhen));
562      if (config != null && !config.isEmpty())
563         m.put("config", config);
564      return m;
565   }
566
567   /**
568    * One entry in the field catalog: a label, an optional named renderer and its meta, an
569    * optional href template, a paint format, a grid span, and per-field action-bar ids.
570    *
571    * <p>
572    * Card and tab regions only &mdash; see {@link RegionDef#fields}.
573    *
574    * @since 10.0.0
575    */
576   public static class Field {
577
578      /** The key into {@code ctx.data}'s values map. Unique across the enclosing {@link RegionDef}. */
579      public String data;
580
581      /** The label shown above the value. <jk>null</jk> falls back to {@link #data} at paint time. */
582      public String label;
583
584      /** Optional named renderer id, resolved against the same {@code NS._renderers} registry as today. */
585      public String render;
586
587      /** Optional metadata handed to {@link #render}'s renderer function. */
588      public Map<String,Object> renderMeta;
589
590      /** Optional {@code {property}} URL template consumed by the {@code linked} renderer (and any renderer
591       * that reads it). Independent of {@link #render}. */
592      public String href;
593
594      /** How the value is painted. <jk>null</jk> means {@link FieldFormat#TEXT}. */
595      public FieldFormat format;
596
597      /** How many of the grid's columns this field occupies. <jk>null</jk> means {@link FieldSpan#ONE}. */
598      public FieldSpan span;
599
600      /**
601       * Action ids painted beside this field's value. Ids only &mdash; the bar itself is rendered by the
602       * client helper, not by this bean.
603       */
604      public List<String> actions;
605
606      /**
607       * Creates a field bound to the specified values-map key.
608       *
609       * @param data The {@code ctx.data} key. Must not be <jk>null</jk> or blank.
610       * @return A new {@link Field}.
611       */
612      public static Field of(String data) {
613         if (ib(data))
614            throw iaex("RegionDef.Field data must not be null or blank.");
615         var f = new Field();
616         f.data = data;
617         return f;
618      }
619
620      /**
621       * Sets the label.
622       *
623       * @param value The label. An empty string suppresses it (full-width markdown/sanitizedHtml body).
624       * @return This object.
625       */
626      public Field label(String value) {
627         label = value;
628         return this;
629      }
630
631      /**
632       * Sets the named renderer id.
633       *
634       * @param value The renderer id.
635       * @return This object.
636       */
637      public Field render(String value) {
638         render = value;
639         return this;
640      }
641
642      /**
643       * Sets the renderer's metadata.
644       *
645       * @param value The metadata.
646       * @return This object.
647       */
648      public Field renderMeta(Map<String,Object> value) {
649         renderMeta = value;
650         return this;
651      }
652
653      /**
654       * Sets the declarative {@code {property}} URL template.
655       *
656       * @param value The template.
657       * @return This object.
658       */
659      public Field href(String value) {
660         href = value;
661         return this;
662      }
663
664      /**
665       * Sets the paint format.
666       *
667       * @param value The format. <jk>null</jk> means {@link FieldFormat#TEXT}.
668       * @return This object.
669       */
670      public Field format(FieldFormat value) {
671         format = value;
672         return this;
673      }
674
675      /**
676       * Sets the grid span.
677       *
678       * @param value The span. <jk>null</jk> means {@link FieldSpan#ONE}.
679       * @return This object.
680       */
681      public Field span(FieldSpan value) {
682         span = value;
683         return this;
684      }
685
686      /**
687       * Sets the per-field action ids.
688       *
689       * @param value The action ids.
690       * @return This object.
691       */
692      public Field actions(String...value) {
693         actions = l(value);
694         return this;
695      }
696
697      /**
698       * The rules that must all match for this one detail field to render.  <jk>null</jk> or empty means always
699       * visible.  See {@link RowAction#visibleWhen} for the shared semantics.
700       */
701      public List<VisibilityRule> visibleWhen;
702
703      /**
704       * Hides this field unless every rule matches.
705       *
706       * @param rules The rules; all must match.  Replaces any rules set by an earlier call.
707       * @return This object.
708       */
709      public Field visibleWhen(VisibilityRule...rules) {
710         visibleWhen = l(rules);
711         return this;
712      }
713
714      /**
715       * Fail-closed validation of this one entry: a non-blank {@link #data}.
716       *
717       * @param regionId The enclosing region's id, for the error message only.
718       */
719      void validate(String regionId) {
720         if (ib(data))
721            throw iaex("RegionDef '%s' field data must not be null or blank.", regionId);
722      }
723
724      /**
725       * Serializes this entry to the compact JSON shape of the sidecar contract.
726       *
727       * @return An ordered {@link Map} ready for {@code Json.of(...)}.
728       */
729      public Map<String,Object> toContractMap() {
730         var m = new LinkedHashMap<String,Object>();
731         m.put("data", data);
732         if (label != null)
733            m.put("label", label);
734         if (render != null)
735            m.put("render", render);
736         if (renderMeta != null && !renderMeta.isEmpty())
737            m.put("renderMeta", renderMeta);
738         if (href != null)
739            m.put("href", href);
740         if (format != null && format != FieldFormat.TEXT)
741            m.put("format", format.wire());
742         if (span == FieldSpan.FULL)
743            m.put("span", "full");
744         if (actions != null && !actions.isEmpty())
745            m.put("actions", actions);
746         if (visibleWhen != null && !visibleWhen.isEmpty())
747            m.put("visibleWhen", VisibilityRule.toMaps(visibleWhen));
748         return m;
749      }
750   }
751}