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 — 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 — unlike an arbitrary 106 * function body — 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 — 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" — 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} — 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 — 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 — 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 — 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) — 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 — 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 — 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 — 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}