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.widgets;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.bean.*;
024
025/**
026 * The declarative modal (dialog) a {@code present=dialog} row action opens — the payload the
027 * <b>modal-open confirmation fetch</b> returns.
028 *
029 * <h5 class='section'>A widget contract driven by a view-module runtime</h5>
030 * <p>
031 * This bean is a general widget contract and lives in this module, but the browser runtime that paints it ships with
032 * the rich-view module (it is part of that module's {@code juneau-views.js} monolith, which is not split apart here).
033 * So a page that opens one of these dialogs loads the view runtime; nothing in this module reaches back to it.
034 *
035 * <h5 class='section'>Safe-by-construction confirmation body</h5>
036 * <p>
037 * The confirmation body is <b>typed structured fields</b> ({@link #fields}: incident #, title, service, current
038 * status &mdash; the confirmation field set), server-rendered against the current, authoritative record and
039 * bound to the target identifier.  The {@code juneau-views.js} runtime paints each field's value with
040 * {@code textContent} &mdash; NEVER {@code innerHTML}, never raw markup, never the non-table panel-content sink.
041 * Live/remote data is attacker-influenceable (anyone who can title an incident or post in a watched thread) and this
042 * origin holds the CSRF token, so an HTML sink here would be stored-XSS&nbsp;&rarr;&nbsp;token-theft&nbsp;&rarr;
043 * arbitrary-write; typed fields painted with {@code textContent} close that off by construction.
044 *
045 * <h5 class='section'>The idempotency key rides here</h5>
046 * <p>
047 * {@link #idempotencyKey} is a server-minted key value, minted at modal-open and bound to
048 * {@code (action, targetId)}.  The runtime carries it verbatim on the submit so a double-click / re-submit / browser
049 * retry all collapse to one effect; the server checks that binding on submit and answers a mismatch with a named
050 * refusal, never a replayed success.  The minting/binding helper itself is a table-and-row-action concern and stays
051 * in the rich-view module, which is why this field is a plain {@code String} rather than that helper's type.
052 *
053 * <h5 class='section'>{@link #selfTargeted} &mdash; the opt-in for a key that targets itself</h5>
054 * <p>
055 * A dialog opened where there is <b>no row</b> (a ribbon-hosted dialog whose submit creates the thing it is about)
056 * has no artifact id to bind its key to at modal-open.  The rich-view module's key helper can mint a key whose
057 * bound target <i>is its own value</i>; {@link #selfTargeted} is the signal that tells the runtime to send that
058 * value as the submitted target instead of the row's id.
059 * <p>
060 * The signal has to ride on this bean, because it cannot be inferred from the wire: {@link #idempotencyKey} is an
061 * opaque string either way, indistinguishable in shape from an artifact-bound key's value.  It is therefore an
062 * explicit, additive, per-dialog opt-in &mdash; <b>not</b> a blanket runtime precedence rule.  A dialog that leaves
063 * this unset behaves exactly as it does today, <i>including</i> one that sets {@link #idempotencyKey} for an
064 * artifact-bound reason: it keeps sending the row's real id, which is what its own mint call bound.
065 * <p>
066 * Set it <b>only</b> on a modal whose key came from the self-targeted mint helper.  Setting it on a modal carrying
067 * an artifact-bound key silently discards the real target and makes the server's binding check vacuous.
068 *
069 * <h5 class='section'>{@link #keepOpenOnSubmit} &mdash; the opt-in for an in-dialog result receipt</h5>
070 * <p>
071 * By default the runtime closes a dialog at confirm-click time and then submits, so every dialog is gone before its
072 * result exists.  {@link #keepOpenOnSubmit} suppresses that close, and the runtime instead paints the write's
073 * <b>result</b> into the dialog that is already open &mdash; a <i>receipt</i>.
074 * <p>
075 * A receipt host is <b>non-submittable by construction</b>, not by convention: the runtime paints it with a distinct
076 * function that creates no confirm control at all, and it refuses to paint one whose payload carries a
077 * {@link #form}.  It is reached <b>only</b> after a committed write (an
078 * {@code ActionResult} whose outcome is {@code success}), so a result host can never appear for a write that did not
079 * happen.  A follow-up read-only form source is named by that result's {@code resultForm}.
080 * <p>
081 * The opt-in has to live here rather than on the action bean, because the confirm handler must decide whether to
082 * close <i>before</i> any response exists.  A dialog synthesized from a {@code confirm} string alone (no form-source
083 * URL) therefore cannot opt in &mdash; a receipt-bearing action must serve a real {@link ModalDef}.
084 * <p>
085 * Additive, and named degradation, accepted: an older runtime ignoring this flag closes the dialog and drops the
086 * receipt.  The write still happens; the receipt is simply not shown.
087 *
088 * <h5 class='section'>{@link #childActions} &mdash; the dialog-scoped child-action catalog</h5>
089 * <p>
090 * A {@code type="action"} form input names an action id, and the runtime resolves it against the enclosing view's
091 * row-action catalog.  A <b>stacked</b> step reached from inside an already-open dialog (a Review step in a wizard)
092 * has nothing legal to resolve against there: it is not a row action, and publishing it as one would surface it in
093 * every row's action menu.  {@link #childActions} is that missing catalog, scoped to <b>one open dialog</b>.
094 * <p>
095 * Scoping is <b>structural</b>, not a flag some menu builder has to remember to filter on: this catalog rides on a
096 * per-open payload, so the row-action menu and the ribbon resolver never see it and there is no exclusion logic to
097 * rot.  Resolution precedence is likewise fail-safe: the existing row-action check runs <b>first</b> and unchanged,
098 * so a served payload can never shadow &mdash; and thereby bypass the gating of &mdash; a declared row action.
099 *
100 * <h5 class='section'>{@link #chrome} and {@link #steps} &mdash; dialog styling and a built-in wizard</h5>
101 * <p>
102 * {@link #chrome} names a dialog chrome the page script registered with the runtime; the runtime hands that chrome the
103 * freshly built dialog parts so it can restyle them.  An unregistered name is ignored and the default chrome is kept.
104 * <p>
105 * {@link #steps} turns the dialog into a wizard: the runtime shows one {@link Step} at a time with Back and Next
106 * buttons, and the last step's button is the dialog's own confirm.  Both are additive-only and omitted from the wire
107 * when unset, so neither bumps {@link #CONTRACT_VERSION}.
108 *
109 * <h5 class='section'>Not a new row-action wire field</h5>
110 * <p>
111 * This is the response a row action's form-source URL returns, <b>not</b> a new field on the frozen row-action wire
112 * schema: the modal's declaration lives in this separately-served payload so the row-action contract stays frozen.
113 * The same reasoning is why {@link #keepOpenOnSubmit} and {@link #childActions} land here rather than on
114 * {@code RowAction} / {@code RibbonAction}, which stay frozen and untouched.
115 *
116 * <h5 class='section'>Example:</h5>
117 * <p class='bjava'>
118 *    ModalDef <jv>modal</jv> = ModalDef.<jsm>create</jsm>(<js>"Acknowledge this incident?"</js>)
119 *       .field(<js>"Incident"</js>, <jv>incidentNumber</jv>)
120 *       .field(<js>"Title"</js>, <jv>incidentTitle</jv>)
121 *       .field(<js>"Service"</js>, <jv>serviceName</jv>)
122 *       .field(<js>"Current status"</js>, <jv>currentStatus</jv>)
123 *       .form(FormDef.<jsm>ofTemplate</jsm>(<js>"servlet:/incidents/ack-form.ftl"</js>))
124 *       .idempotencyKey(<jv>keyValue</jv>);
125 * </p>
126 *
127 * <h5 class='section'>See Also:</h5>
128 * <ul>
129 *    <li class='jc'>{@link FormDef}
130 *    <li class='jc'>{@link Widget}
131 * </ul>
132 *
133 * <h5 class='section'>The third named bar-slot host</h5>
134 * <p>
135 * {@link #barSlot} is the <i>third</i> named {@link BarSlot} attachment &mdash; alongside the rich-view module's page
136 * and row-detail hosts (that module never depends on this one, so they are not linked here).  Unlike those two
137 * &mdash; Java-only fields a server-side emitter paints directly, omitted from their wire &mdash; this modal itself
138 * <b>is</b> the wire payload the modal-open confirmation fetch returns, with no separate server-rendered pass the
139 * dialog could ride into.  So this field <b>is</b> carried on the wire (additive-only: {@code null} when unset, and
140 * its presence does not bump {@link #CONTRACT_VERSION}), and the {@code juneau-views.js} runtime paints the region
141 * and its dynamic-count sidecar client-side from the JSON, the same way it already paints typed confirmation
142 * {@link #fields} and {@link #form} controls &mdash; {@code textContent} only, never {@code innerHTML}.  The dialog
143 * anchors its own region immediately after the title (the {@code ANCHOR_DIALOG_TITLE}
144 * placement in {@code juneau-views.js}), owning that placement itself rather than leaning on the shared strip builder,
145 * exactly as the row-detail host owns its own ribbon-trailing relocation.
146 *
147 * @since 10.0.0
148 */
149@BeanType(properties="contractVersion,title,fields,form,idempotencyKey,selfTargeted,barSlot,keepOpenOnSubmit,childActions,chrome,steps")
150@SuppressWarnings({
151   "java:S1845", // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
152   "java:S3776" // Branching is inherent to validate() and validateChildActions(), which check many field/child-action combinations.
153})
154public class ModalDef implements Widget {
155
156   /**
157    * The frozen modal contract version.  Bumped only on a breaking wire change to this modal contract.
158    *
159    * <p>
160    * Moves in <b>lockstep</b> with {@link FormDef#CONTRACT_VERSION} and the runtime's baked-in literal: the runtime
161    * compares one literal against both this version and the nested form's, so bumping any two of the three makes
162    * every form-bearing dialog refuse to open.
163    */
164   public static final String CONTRACT_VERSION = "2";
165
166   /** The {@link Field#kind} token for an ordinary text value &mdash; the default when the field declares none. */
167   public static final String FIELD_KIND_TEXT = "text";
168
169   /** The {@link Field#kind} token for a monospaced, copyable value (an id, a URL, a generated token). */
170   public static final String FIELD_KIND_CODE = "code";
171
172   /**
173    * A single typed confirmation field: a label and a value, painted client-side with {@code textContent} (never
174    * {@code innerHTML}).
175    *
176    * @since 10.0.0
177    */
178   @BeanType(properties="label,value,kind")
179   public static class Field {
180
181      /** The field label shown to the user. */
182      public String label;
183
184      /** The field value, read back from the current authoritative record. */
185      public String value;
186
187      /**
188       * How this field's value is <i>displayed</i>; omitted from the wire when unset (which reads as
189       * {@link ModalDef#FIELD_KIND_TEXT text}).
190       *
191       * <p>
192       * The allowlist is exactly {@link ModalDef#FIELD_KIND_TEXT text} and {@link ModalDef#FIELD_KIND_CODE code};
193       * {@link ModalDef#validate()} rejects anything else server-side, and the runtime falls back to
194       * {@code text} for an unrecognized token rather than trusting it.
195       *
196       * <p>
197       * This is a <b>display</b> kind on a confirmation field, deliberately <b>not</b> a new
198       * {@code FormDef.Input} type: a {@code code} field carries no submit value, so expressing it as an input
199       * would mean teaching the collection, validation, prefill and submit paths about a control that is not one.
200       */
201      public String kind;
202
203      /**
204       * Creates a typed confirmation field.
205       *
206       * @param label The field label.  Must not be <jk>null</jk> or blank.
207       * @param value The field value.  Can be <jk>null</jk> (rendered as an empty value).
208       * @return A new {@link Field}.
209       * @throws IllegalArgumentException If {@code label} is <jk>null</jk> or blank.
210       */
211      public static Field of(String label, String value) {
212         if (ib(label))
213            throw iaex("ModalDef.Field label must not be null or blank.");
214         var f = new Field();
215         f.label = label;
216         f.value = value;
217         return f;
218      }
219
220      /**
221       * Creates a typed confirmation field displayed as monospaced, copyable {@link ModalDef#FIELD_KIND_CODE code}.
222       *
223       * <p>
224       * The runtime paints the value into a {@code <pre>} with {@code textContent} (never {@code innerHTML}) and
225       * adds a copy button that is feature-detected and never throws; the {@code <pre>} stays selectable, so
226       * manual copy always works.
227       *
228       * @param label The field label.  Must not be <jk>null</jk> or blank.
229       * @param value The field value.  Can be <jk>null</jk> (rendered as an empty value).
230       * @return A new {@link Field}.
231       * @throws IllegalArgumentException If {@code label} is <jk>null</jk> or blank.
232       */
233      public static Field code(String label, String value) {
234         var f = of(label, value);
235         f.kind = FIELD_KIND_CODE;
236         return f;
237      }
238   }
239
240   /**
241    * One page of a {@link ModalDef#steps} wizard: an id, an optional title and confirm-button label, and the typed
242    * display fields painted for that page.
243    *
244    * @since 10.0.0
245    */
246   @BeanType(properties="id,title,confirmLabel,fields")
247   public static class Step {
248
249      /** The step id, unique within the modal; a script selects a step by it.  Must not be blank. */
250      public String id;
251
252      /** The dialog title shown while this step is current; omitted from the wire when unset (the modal title stays). */
253      public String title;
254
255      /** The confirm-button label for this step; omitted when unset (the runtime shows Next, or the final label on the last step). */
256      public String confirmLabel;
257
258      /** The typed display fields painted for this step, in display order; omitted from the wire when none are declared. */
259      public List<Field> fields;
260
261      /**
262       * Creates a step.
263       *
264       * @param id The step id.  Must not be <jk>null</jk> or blank.
265       * @return A new {@link Step}.
266       * @throws IllegalArgumentException If {@code id} is <jk>null</jk> or blank.
267       */
268      public static Step of(String id) {
269         if (ib(id))
270            throw iaex("ModalDef.Step id must not be null or blank.");
271         var s = new Step();
272         s.id = id;
273         return s;
274      }
275
276      /**
277       * Sets the dialog title shown while this step is current.
278       *
279       * @param value The title.  Can be <jk>null</jk> to unset.
280       * @return This object.
281       */
282      public Step title(String value) {
283         title = value;
284         return this;
285      }
286
287      /**
288       * Sets the confirm-button label for this step.
289       *
290       * @param value The label.  Can be <jk>null</jk> to unset.
291       * @return This object.
292       */
293      public Step confirmLabel(String value) {
294         confirmLabel = value;
295         return this;
296      }
297
298      /**
299       * Adds one typed display field to this step.
300       *
301       * @param label The field label.  Must not be <jk>null</jk> or blank.
302       * @param value The field value.  Can be <jk>null</jk>.
303       * @return This object.
304       */
305      public Step field(String label, String value) {
306         if (fields == null)
307            fields = l();
308         fields.add(Field.of(label, value));
309         return this;
310      }
311   }
312
313   /**
314    * One entry in a dialog's {@link ModalDef#childActions} catalog &mdash; a stacked step a {@code type="action"}
315    * input inside <i>this</i> dialog's form may open.
316    *
317    * <h5 class='section'>Dialog-presenting by construction</h5>
318    * <p>
319    * A child action carries no {@code present} field: opening a stacked dialog is the only thing it does.  A child
320    * with no {@link #form} is a legal confirm-only child dialog.
321    *
322    * <h5 class='section'>No gating vocabulary, on purpose</h5>
323    * <p>
324    * There is deliberately no {@code enabledWhen} here, for three independent reasons.  The rule type lives in the
325    * rich-view module, which this module must not reference.  A dialog opened where there is no row has no row data
326    * to evaluate against, so a field-keyed rule would fail closed forever with no signal to its author.  And the
327    * subject of a child step is the <i>draft in the open dialog</i>, not the parent row &mdash; so evaluating the
328    * parent row's data would answer a question nobody asked.  Gating a child step is the <b>server's</b> job at
329    * child-form-GET time; it already owns that GET and can refuse it.
330    *
331    * @since 10.0.0
332    */
333   @BeanType(properties="id,label,form,endpoint,method,onSuccess,carryDrafts")
334   public static class ChildAction {
335
336      /** The action id a {@code type="action"} input in this dialog's form names.  Required, non-blank. */
337      public String id;
338
339      /** The label painted on the button that opens this child.  Required, non-blank. */
340      public String label;
341
342      /**
343       * The child dialog's read-only form-source URL; omitted from the wire when unset (a confirm-only child).
344       *
345       * <p>
346       * Required if this child declares {@link #carryDrafts} &mdash; with no URL there is nothing for the draft
347       * query to ride on, so the drafts would vanish silently.
348       */
349      public String form;
350
351      /** The child's submit endpoint; omitted from the wire when unset.  See {@link #endpoint(String)}. */
352      public String endpoint;
353
354      /** The child's submit method; omitted from the wire when unset.  See {@link #method(String)}. */
355      public String method;
356
357      /** The child's on-success behaviour token; omitted from the wire when unset. */
358      public String onSuccess;
359
360      /**
361       * Whether opening this child carries the <b>parent dialog's current draft values</b> to its form-source GET
362       * as one query parameter; omitted from the wire when unset or <jk>false</jk>.
363       *
364       * <p>
365       * Without it a Review step opens <b>empty</b> and loses everything the operator typed.  With it, the
366       * runtime serializes the parent's collected form values, URL-encodes them, and appends them as a single
367       * {@code juneauDrafts} parameter, refusing visibly (and not opening the child) above a client-side byte cap
368       * measured on the <i>encoded</i> value.
369       *
370       * <p>
371       * <b>Drafts land in access logs and browser history.</b>  No secret may ride this channel.
372       */
373      public Boolean carryDrafts;
374
375      /**
376       * Creates a child action.
377       *
378       * @param id The action id a {@code type="action"} input names.  Must not be <jk>null</jk> or blank.
379       * @param label The button label.  Must not be <jk>null</jk> or blank.
380       * @return A new {@link ChildAction}.
381       * @throws IllegalArgumentException If {@code id} or {@code label} is <jk>null</jk> or blank.
382       */
383      public static ChildAction of(String id, String label) {
384         if (ib(id))
385            throw iaex("ModalDef.ChildAction id must not be null or blank.");
386         if (ib(label))
387            throw iaex("ModalDef.ChildAction label must not be null or blank.");
388         var c = new ChildAction();
389         c.id = id;
390         c.label = label;
391         return c;
392      }
393
394      /**
395       * Sets the child dialog's read-only form-source URL.
396       *
397       * @param value The URL.  Can be <jk>null</jk> to unset (a confirm-only child).
398       * @return This object.
399       */
400      public ChildAction form(String value) {
401         form = value;
402         return this;
403      }
404
405      /**
406       * Sets the child's submit endpoint.
407       *
408       * <p>
409       * Optional individually; a child action whose dialog is meant to be <b>submittable</b> must declare both a
410       * non-safe {@link #method} and an {@code endpoint}, or its Confirm visibly refuses with {@code safe-method}
411       * and sends nothing.  A display-only child that never submits needs neither, which is why this is not a
412       * {@link ModalDef#validate()} rule &mdash; this bean cannot know which of its children are meant to submit.
413       *
414       * @param value The endpoint.  Can be <jk>null</jk> to unset.
415       * @return This object.
416       */
417      public ChildAction endpoint(String value) {
418         endpoint = value;
419         return this;
420      }
421
422      /**
423       * Sets the child's submit method.
424       *
425       * <p>
426       * Optional individually; see {@link #endpoint(String)} &mdash; a submittable child needs both, and a
427       * missing or safe method is a visible client-side refusal before any request is sent.
428       *
429       * @param value The method.  Can be <jk>null</jk> to unset.
430       * @return This object.
431       */
432      public ChildAction method(String value) {
433         method = value;
434         return this;
435      }
436
437      /**
438       * Sets the child's on-success behaviour token.
439       *
440       * @param value The token.  Can be <jk>null</jk> to unset.
441       * @return This object.
442       */
443      public ChildAction onSuccess(String value) {
444         onSuccess = value;
445         return this;
446      }
447
448      /**
449       * Declares that opening this child carries the parent dialog's drafts (see {@link #carryDrafts}).
450       *
451       * <p>
452       * Passing <jk>false</jk> <b>clears</b> the flag rather than serializing an explicit {@code false}, so an
453       * un-opted-in child's payload is byte-identical to one that never mentioned it.
454       *
455       * @param value <jk>true</jk> to carry the parent's drafts; <jk>false</jk> (or unset) to open empty.
456       * @return This object.
457       */
458      public ChildAction carryDrafts(boolean value) {
459         carryDrafts = value ? Boolean.TRUE : null;
460         return this;
461      }
462   }
463
464   /**
465    * The frozen modal contract version discriminator.
466    *
467    * <p>
468    * Defaults to {@link #CONTRACT_VERSION} on <b>every</b> modal, confirm-only or form-bearing, from the instant
469    * it is constructed &mdash; the guarantee does not depend on {@link #checked()} being called.
470    * Guaranteed <b>present</b> is not the same as guaranteed <b>valid</b>: presence is this field's whole job,
471    * structural well-formedness is {@link #validate()}'s (see {@link #checked()}).
472    */
473   public String contractVersion = CONTRACT_VERSION;
474
475   /** The modal title / confirmation prompt. */
476   public String title;
477
478   /** The typed confirmation fields, in display order; omitted from the wire when none are declared. */
479   public List<Field> fields;
480
481   /** The optional input form; omitted from the wire when unset (a confirm-only modal). */
482   public FormDef form;
483
484   /**
485    * The server-minted idempotency key value the runtime carries on the submit; omitted from the wire when unset.
486    * The value is minted (and its {@code (action, targetId)} binding re-checked on submit) by the rich-view
487    * module's idempotency-key helper.
488    */
489   public String idempotencyKey;
490
491   /**
492    * Whether {@link #idempotencyKey} is a <b>self-targeted</b> key whose own value the runtime must send as the
493    * submitted target (see the class Javadoc); omitted from the wire when unset or <jk>false</jk>.
494    *
495    * <p>
496    * Additive-only: like {@link #barSlot}, its presence does not bump {@link #CONTRACT_VERSION}.
497    */
498   public Boolean selfTargeted;
499
500   /**
501    * Optional additive bar slot anchored to this dialog's title &mdash; the <i>third</i> named {@link BarSlot}
502    * attachment (see the class Javadoc for how this host differs from the other two).  Omitted from the wire when
503    * unset; painted by the runtime immediately after {@link #title}, ahead of {@link #fields} and {@link #form}.
504    */
505   public BarSlot barSlot;
506
507   /**
508    * Whether the runtime must <b>not</b> close this dialog at confirm-click time, so the write's result can be
509    * painted into it as a receipt (see the class Javadoc); omitted from the wire when unset or <jk>false</jk>.
510    *
511    * <p>
512    * Additive-only: like {@link #barSlot} and {@link #selfTargeted}, its presence does not bump
513    * {@link #CONTRACT_VERSION}.
514    */
515   public Boolean keepOpenOnSubmit;
516
517   /**
518    * The dialog-scoped catalog of stacked child actions a {@code type="action"} input in {@link #form} may open
519    * (see the class Javadoc); omitted from the wire when unset or empty.
520    */
521   public List<ChildAction> childActions;
522
523   /**
524    * The name of a dialog chrome the page script registered with the runtime, which restyles this dialog's parts
525    * (see the class Javadoc); omitted from the wire when unset (the default chrome).
526    *
527    * <p>
528    * Additive-only: like {@link #barSlot}, its presence does not bump {@link #CONTRACT_VERSION}.
529    */
530   public String chrome;
531
532   /**
533    * The wizard steps the runtime shows one at a time with Back and Next (see the class Javadoc); omitted from the
534    * wire when unset or empty.
535    *
536    * <p>
537    * Additive-only: like {@link #barSlot}, its presence does not bump {@link #CONTRACT_VERSION}.
538    */
539   public List<Step> steps;
540
541   /**
542    * Starts a new {@link ModalDef} with the specified title / confirmation prompt.
543    *
544    * @param title The modal title / confirmation prompt.  Must not be <jk>null</jk> or blank.
545    * @return A new mutable {@link ModalDef} to chain builder calls on.
546    * @throws IllegalArgumentException If {@code title} is <jk>null</jk> or blank.
547    */
548   public static ModalDef create(String title) {
549      if (ib(title))
550         throw iaex("ModalDef title must not be null or blank.");
551      var m = new ModalDef();
552      m.title = title;
553      return m;
554   }
555
556   /**
557    * Adds one typed confirmation field.
558    *
559    * @param label The field label.  Must not be <jk>null</jk> or blank.
560    * @param value The field value (read back from the current record).  Can be <jk>null</jk>.
561    * @return This object.
562    */
563   public ModalDef field(String label, String value) {
564      if (fields == null)
565         fields = l();
566      fields.add(Field.of(label, value));
567      return this;
568   }
569
570   /**
571    * Adds one typed confirmation field displayed as monospaced, copyable {@link #FIELD_KIND_CODE code}.
572    *
573    * @param label The field label.  Must not be <jk>null</jk> or blank.
574    * @param value The field value.  Can be <jk>null</jk>.
575    * @return This object.
576    */
577   public ModalDef codeField(String label, String value) {
578      if (fields == null)
579         fields = l();
580      fields.add(Field.code(label, value));
581      return this;
582   }
583
584   /**
585    * Adds one entry to this dialog's child-action catalog.
586    *
587    * @param value The child action.  Must not be <jk>null</jk>.
588    * @return This object.
589    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
590    */
591   public ModalDef childAction(ChildAction value) {
592      if (value == null)
593         throw iaex("ModalDef.ChildAction must not be null.");
594      if (childActions == null)
595         childActions = l();
596      childActions.add(value);
597      return this;
598   }
599
600   /**
601    * Selects the dialog chrome the page script registered under the given name.
602    *
603    * @param value The chrome name.  Can be <jk>null</jk> to unset (the default chrome); a non-null value must not be blank.
604    * @return This object.
605    */
606   public ModalDef chrome(String value) {
607      chrome = value;
608      return this;
609   }
610
611   /**
612    * Adds one wizard step.
613    *
614    * @param value The step.  Must not be <jk>null</jk>.
615    * @return This object.
616    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
617    */
618   public ModalDef step(Step value) {
619      if (value == null)
620         throw iaex("ModalDef.Step must not be null.");
621      if (steps == null)
622         steps = l();
623      steps.add(value);
624      return this;
625   }
626
627   /**
628    * Declares that the runtime must keep this dialog open across its submit so the result can be painted into it
629    * as a receipt (see the class Javadoc).
630    *
631    * <p>
632    * Passing <jk>false</jk> <b>clears</b> the flag rather than serializing an explicit {@code false}: the wire
633    * carries this opt-in only when it is on, so an un-opted-in dialog's payload is byte-identical to one that never set it.
634    *
635    * @param value <jk>true</jk> to opt in; <jk>false</jk> (or unset) keeps the default close-then-submit behaviour.
636    * @return This object.
637    */
638   public ModalDef keepOpenOnSubmit(boolean value) {
639      keepOpenOnSubmit = value ? Boolean.TRUE : null;
640      return this;
641   }
642
643   /**
644    * Sets the optional input form.
645    *
646    * @param value The form.  Can be <jk>null</jk> to unset (a confirm-only modal).
647    * @return This object.
648    */
649   public ModalDef form(FormDef value) {
650      form = value;
651      return this;
652   }
653
654   /**
655    * Sets the server-minted idempotency key value carried on the submit.
656    *
657    * @param value The key value.  Can be <jk>null</jk> to unset.
658    * @return This object.
659    */
660   public ModalDef idempotencyKey(String value) {
661      idempotencyKey = value;
662      return this;
663   }
664
665   /**
666    * Declares that {@link #idempotencyKey} is a self-targeted key &mdash; the runtime sends the key's own value as
667    * the submitted target rather than a row id.
668    *
669    * <p>
670    * Set this <b>only</b> alongside a key minted by the rich-view module's self-targeted mint helper; pairing it
671    * with an artifact-bound key discards that key's real target.  See the class Javadoc.
672    *
673    * <p>
674    * Passing <jk>false</jk> <b>clears</b> the flag rather than serializing an explicit {@code false}: the wire
675    * carries this opt-in only when it is on, so an un-opted-in dialog's payload is byte-identical to one that never set it.
676    *
677    * @param value <jk>true</jk> to opt in; <jk>false</jk> (or unset) keeps the default row-id behaviour.
678    * @return This object.
679    */
680   public ModalDef selfTargeted(boolean value) {
681      selfTargeted = value ? Boolean.TRUE : null;
682      return this;
683   }
684
685   /**
686    * Declares the additive bar slot anchored to this dialog's title.
687    *
688    * <p>
689    * See {@link #barSlot} &mdash; a third named host for the same {@link BarSlot} bean, not a re-use of the
690    * rich-view module's page or row-detail hosts.
691    *
692    * @param value The bar slot.  Can be <jk>null</jk> (no dialog bar slot).
693    * @return This object.
694    */
695   public ModalDef barSlot(BarSlot value) {
696      barSlot = value;
697      return this;
698   }
699
700   /**
701    * Fail-closed bean validation.
702    *
703    * <p>
704    * Requires a non-blank {@link #title} and a non-blank label on each {@link Field}; when a {@link #form} is present
705    * it delegates to {@link FormDef#validate()}.  Does <b>not</b> require {@link #contractVersion} to be set to
706    * {@link #CONTRACT_VERSION} (or set at all) &mdash; a modal whose version was explicitly cleared, or that
707    * arrived from a foreign producer that never set one, must not false-refuse on that alone.  Version-presence
708    * and structural well-formedness are deliberately independent properties (design note, &sect;5.4).
709    *
710    * <p>
711    * Also rejects, fail-closed at serve time rather than silently on the wire: a {@link Field#kind} outside the
712    * {@link #FIELD_KIND_TEXT text}/{@link #FIELD_KIND_CODE code} allowlist; a blank, duplicate or unlabelled
713    * {@link ChildAction} id; {@link ChildAction#carryDrafts} on a modal that declares no {@link #form} (nothing to
714    * collect <i>from</i>); and {@link ChildAction#carryDrafts} on a child that declares no
715    * {@link ChildAction#form} (nowhere to put them &mdash; the runtime would take its confirm-only branch, issue
716    * no GET, and the drafts would vanish silently).
717    *
718    * @throws IllegalArgumentException If this modal is not well-formed.
719    */
720   @Override
721   public void validate() {
722      if (ib(title))
723         throw iaex("ModalDef title must not be null or blank.");
724      if (fields != null)
725         for (var f : fields) {
726            if (f == null)
727               throw iaex("ModalDef field must not be null.");
728            if (f.label == null || f.label.isBlank())
729               throw iaex("ModalDef.Field label must not be null or blank.");
730            if (f.kind != null && ! (eqa(f.kind, FIELD_KIND_TEXT, FIELD_KIND_CODE)))
731               throw iaex("ModalDef.Field kind must be '" + FIELD_KIND_TEXT + "' or '" + FIELD_KIND_CODE
732                  + "', not '" + f.kind + "'.");
733         }
734      validateChildActions();
735      validateSteps();
736      if (chrome != null && chrome.isBlank())
737         throw iaex("ModalDef chrome must not be blank.");
738      if (form != null)
739         form.validate();
740      if (barSlot != null)
741         barSlot.validate();
742   }
743
744   /** The {@link #steps} half of {@link #validate()}: non-blank unique ids and well-formed display fields. */
745   private void validateSteps() {
746      if (steps == null)
747         return;
748      var seen = new HashSet<String>();
749      for (var st : steps) {
750         if (st == null)
751            throw iaex("ModalDef.Step must not be null.");
752         if (st.id == null || st.id.isBlank())
753            throw iaex("ModalDef.Step id must not be null or blank.");
754         if (! seen.add(st.id))
755            throw iaex("ModalDef.Step id '" + st.id + "' is declared more than once on this modal.");
756         if (st.fields != null)
757            for (var f : st.fields) {
758               if (f == null || f.label == null || f.label.isBlank())
759                  throw iaex("ModalDef.Step '" + st.id + "' field label must not be null or blank.");
760               if (f.kind != null && ! (eqa(f.kind, FIELD_KIND_TEXT, FIELD_KIND_CODE)))
761                  throw iaex("ModalDef.Step '" + st.id + "' field kind must be '" + FIELD_KIND_TEXT + "' or '"
762                     + FIELD_KIND_CODE + "', not '" + f.kind + "'.");
763            }
764      }
765   }
766
767   /**
768    * The {@link #childActions} half of {@link #validate()}.
769    *
770    * <p>
771    * {@link ChildAction#of(String,String)} already argument-checks {@code id} and {@code label}; they are
772    * re-checked here as the wire-level backstop for a bean-deserialized instance that never went through the
773    * factory.
774    */
775   private void validateChildActions() {
776      if (childActions == null)
777         return;
778      var seen = new HashSet<String>();
779      for (var c : childActions) {
780         if (c == null)
781            throw iaex("ModalDef.ChildAction must not be null.");
782         if (c.id == null || c.id.isBlank())
783            throw iaex("ModalDef.ChildAction id must not be null or blank.");
784         if (c.label == null || c.label.isBlank())
785            throw iaex("ModalDef.ChildAction label must not be null or blank.");
786         if (! seen.add(c.id))
787            throw iaex("ModalDef.ChildAction id '" + c.id + "' is declared more than once on this modal.");
788         if (Boolean.TRUE.equals(c.carryDrafts)) {
789            if (form == null)
790               throw iaex("ModalDef.ChildAction '" + c.id + "' declares carryDrafts, but this modal declares "
791                  + "no form - there is nothing to collect drafts from.");
792            if (c.form == null || c.form.isBlank())
793               throw iaex("ModalDef.ChildAction '" + c.id + "' declares carryDrafts, but declares no form URL "
794                  + "- a confirm-only child issues no form GET, so the drafts would be dropped silently.");
795         }
796      }
797   }
798
799   /**
800    * The serving-path hook every app {@code @RestGet} that returns a {@link ModalDef} should invoke.
801    *
802    * <p>
803    * {@link #contractVersion} is guaranteed present on every modal from construction, so this is <b>no longer
804    * required for versioning</b>.  It stays <b>strongly recommended</b> for one remaining duty and
805    * is <b>redundant-but-harmless</b> for another:
806    * <ul>
807    *    <li><b>Non-REST producers</b> (a direct {@code Json.of(...)} serialization, or any consumer marshalling a
808    *       modal outside a REST response) have no other gate: calling this is the only fail-closed structural
809    *       {@link #validate() validation} they get, and skipping it means a malformed modal/form serializes as
810    *       contract-<i>valid</i> with no complaint.
811    *    <li><b>REST responses</b> are validated independently:
812    *       {@link WidgetsMixin.WidgetValidationProcessor
813    *       WidgetsMixin.WidgetValidationProcessor} calls {@link #validate()} on every REST response that is a
814    *       {@code Widget}, fail-closed with a 500, whether or not the producing endpoint called this method.
815    *       Calling it anyway is safe (validation is idempotent) and remains the only gate for the non-REST case
816    *       above.
817    * </ul>
818    * When a {@link #form} is present it also re-stamps {@link #CONTRACT_VERSION} on the form and recurses into
819    * {@link FormDef#checked()}.
820    *
821    * @return This object.
822    * @throws IllegalArgumentException If this modal (or its form) is not well-formed.
823    */
824   public ModalDef checked() {
825      if (form != null) {
826         contractVersion = CONTRACT_VERSION;
827         form.checked();
828      }
829      validate();
830      return this;
831   }
832}