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.*;
022import java.util.regex.*;
023
024import org.apache.juneau.commons.bean.*;
025
026/**
027 * The declarative source of a row-action modal's input form (the form half of a {@code present=dialog} action).
028 *
029 * <h5 class='section'>A widget contract driven by a view-module runtime</h5>
030 * <p>
031 * Like {@link ModalDef}, this is a general widget contract that lives in this module while the browser runtime that
032 * paints it ships with the rich-view module.  Nothing here depends on that module.
033 *
034 * <h5 class='section'>Flat or sectioned</h5>
035 * <p>
036 * A form declares either a flat {@link #fields} list or an ordered list of {@link Section}s, never both: the flat list
037 * <i>is</i> the single-section case.  Sections render as a ribbon-format strip above one visible pane; they are a
038 * layout of one dialog, not a stack of dialogs, so they cost nothing against the runtime's modal-depth cap.
039 *
040 * <h5 class='section'>Typed inputs, not markup</h5>
041 * <p>
042 * The client paints {@link #fields} as native {@code <input>} / {@code <textarea>} / {@code <select>} /
043 * {@code <button>} elements via {@code createElement}.  Labels and option text use {@code textContent}; prefills use
044 * {@code .value} / {@code .checked}.  The runtime never assigns {@code innerHTML} from this payload.  Only the
045 * types on the allowlist ({@code text}, {@code textarea}, {@code password}, {@code checkbox}, {@code toggle}, {@code select},
046 * {@code action}) are legal &mdash; that is the XSS bound: hostile prefill cannot become an element.
047 * </p>
048 * <p>
049 * {@link #template} may name a server-side template for authors.  It is <b>not</b> a client HTML sink; the
050 * shipped {@code juneau-views.js} ignores it.
051 * </p>
052 *
053 * <h5 class='section'>Per-widget contract version (fail-loud when a form is present)</h5>
054 * <p>
055 * This bean {@link Widget#validate() validates} fail-closed and carries an instance {@link #contractVersion}.  The
056 * version defaults to {@link #CONTRACT_VERSION} from the instant the form is constructed &mdash; a raw
057 * builder no longer leaks an unversioned form regardless of whether {@link #checked()} is invoked.  The client
058 * refuses to open a form-bearing dialog whose version does not match the one its runtime bakes in.
059 * </p>
060 * <p>
061 * This constant moves in <b>lockstep</b> with {@link ModalDef#CONTRACT_VERSION} and the runtime's baked-in literal,
062 * because the runtime compares one literal against both the modal's and the form's version.  Bumping any two of the
063 * three makes every form-bearing dialog refuse to open.
064 * </p>
065 *
066 * <h5 class='section'>Example:</h5>
067 * <p class='bjava'>
068 *    FormDef <jv>form</jv> = FormDef.<jsm>create</jsm>()
069 *       .field(FormDef.Input.<jsm>of</jsm>(<js>"resolution"</js>, <js>"Resolution comment"</js>, <js>"textarea"</js>)
070 *          .required()
071 *          .value(<js>""</js>));
072 * </p>
073 *
074 * <h5 class='section'>See Also:</h5>
075 * <ul>
076 *    <li class='jc'>{@link ModalDef}
077 *    <li class='jc'>{@link ActionRef}
078 * </ul>
079 *
080 * @since 10.0.0
081 */
082@BeanType(properties="contractVersion,template,fields,sections")
083@SuppressWarnings({
084   "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
085})
086public class FormDef implements Widget {
087
088   /** The frozen form contract version.  Bumped only on a breaking wire change to this form contract. */
089   public static final String CONTRACT_VERSION = "2";
090
091   /** The cap on the length of an author-supplied {@link Input#pattern} (ReDoS defense-in-depth). */
092   static final int PATTERN_MAX_LENGTH = 256;
093
094   /** The complete set of legal {@link Input#type} tokens. */
095   static final Set<String> INPUT_TYPES = Set.of("text", "textarea", "password", "checkbox", "toggle", "select", "action");
096
097   /**
098    * A single typed form input painted client-side with {@code createElement} (never {@code innerHTML}).
099    *
100    * @since 10.0.0
101    */
102   @BeanType(properties="name,label,type,required,value,options,pattern,maxLength,help,actionId")
103   public static class Input {
104
105      /**
106       * A single {@code select} option: a submit value and its visible label.
107       *
108       * @since 10.0.0
109       */
110      @BeanType(properties="value,label")
111      public static class Option {
112
113         /** The submit value for this option.  Must not be <jk>null</jk>. */
114         public String value;
115
116         /** The visible option text, painted with {@code textContent}.  Must not be blank. */
117         public String label;
118
119         /**
120          * Creates a select option.
121          *
122          * @param value The submit value.  Must not be <jk>null</jk>.
123          * @param label The visible option text.  Must not be <jk>null</jk> or blank.
124          * @return A new {@link Option}.
125          * @throws IllegalArgumentException If {@code value} is <jk>null</jk> or {@code label} is blank.
126          */
127         public static Option of(String value, String label) {
128            if (value == null)
129               throw iaex("FormDef.Input.Option value must not be null.");
130            if (ib(label))
131               throw iaex("FormDef.Input.Option label must not be null or blank.");
132            var o = new Option();
133            o.value = value;
134            o.label = label;
135            return o;
136         }
137      }
138
139      /** The submit-body key for this field (e.g. {@code resolution}). */
140      public String name;
141
142      /** The label shown next to the control. */
143      public String label;
144
145      /** One of {@code text}, {@code textarea}, {@code password}, {@code checkbox}, {@code toggle}, {@code select}, {@code action}. */
146      public String type;
147
148      /** When {@link Boolean#TRUE}, the control is required.  Omitted from the wire otherwise. */
149      public Boolean required;
150
151      /** Optional prefill, applied via {@code .value} / {@code .checked} (never {@code innerHTML}). */
152      public String value;
153
154      /** The options for a {@code select} field, in display order; omitted from the wire otherwise. */
155      public List<Option> options;
156
157      /** An optional client validation regexp for {@code text}/{@code textarea}; omitted from the wire otherwise. */
158      public String pattern;
159
160      /** An optional client max-length for {@code text}/{@code textarea}; omitted from the wire otherwise. */
161      public Integer maxLength;
162
163      /** An optional help hint painted with {@code textContent}; omitted from the wire otherwise. */
164      public String help;
165
166      /**
167       * The enclosing view's row-action id an {@code action} button opens; omitted from the wire otherwise.  See
168       * {@link ActionRef} for the typed reference the builder accepts.
169       */
170      public String actionId;
171
172      /**
173       * Creates a typed form input.
174       *
175       * @param name The submit-body key (also the DOM identity for an {@code action} field).  Must not be
176       *    <jk>null</jk> or blank.
177       * @param label The visible label.  Must not be <jk>null</jk> or blank.
178       * @param type One of {@code text}, {@code textarea}, {@code password}, {@code checkbox}, {@code toggle}, {@code select},
179       *    {@code action}.  <jk>null</jk> or blank defaults to {@code text}.
180       * @return A new {@link Input}.
181       * @throws IllegalArgumentException If {@code name} or {@code label} is blank, or {@code type} is not an
182       *    allowed token.
183       */
184      public static Input of(String name, String label, String type) {
185         if (ib(name))
186            throw iaex("FormDef.Input name must not be null or blank.");
187         if (ib(label))
188            throw iaex("FormDef.Input label must not be null or blank.");
189         var t = (ib(type)) ? "text" : type;
190         if (! INPUT_TYPES.contains(t))
191            throw iaex("FormDef.Input type must be one of text, textarea, password, checkbox, toggle, select, action, not '%s'.", t);
192         var i = new Input();
193         i.name = name;
194         i.label = label;
195         i.type = t;
196         return i;
197      }
198
199      /**
200       * Marks this input required.
201       *
202       * @return This object.
203       */
204      public Input required() {
205         required = Boolean.TRUE;
206         return this;
207      }
208
209      /**
210       * Sets whether this input is required.
211       *
212       * @param value <jk>true</jk> to require a value; <jk>false</jk> omits the flag from the wire.
213       * @return This object.
214       */
215      public Input required(boolean value) {
216         required = value ? Boolean.TRUE : null;
217         return this;
218      }
219
220      /**
221       * Sets the optional prefill.
222       *
223       * @param value The prefill.  Can be <jk>null</jk> to unset.
224       * @return This object.
225       */
226      public Input value(String value) {
227         this.value = value;
228         return this;
229      }
230
231      /**
232       * Sets the {@code select} options (replacing any existing).
233       *
234       * @param value The options.  Must not be <jk>null</jk>.
235       * @return This object.
236       * @throws IllegalArgumentException If {@code value} is <jk>null</jk> or contains a <jk>null</jk> entry.
237       */
238      public Input options(Option... value) {
239         if (value == null)
240            throw iaex("FormDef.Input options must not be null.");
241         var l = new ArrayList<Option>();
242         for (var o : value) {
243            if (o == null)
244               throw iaex("FormDef.Input option must not be null.");
245            l.add(o);
246         }
247         options = l;
248         return this;
249      }
250
251      /**
252       * Adds one {@code select} option.
253       *
254       * @param value The submit value.  Must not be <jk>null</jk>.
255       * @param label The visible option text.  Must not be <jk>null</jk> or blank.
256       * @return This object.
257       * @throws IllegalArgumentException If {@code value} is <jk>null</jk> or {@code label} is blank.
258       */
259      public Input option(String value, String label) {
260         if (options == null)
261            options = new ArrayList<>();
262         options.add(Option.of(value, label));
263         return this;
264      }
265
266      /**
267       * Sets the client validation regexp ({@code text}/{@code textarea} only).
268       *
269       * @param value The pattern.  Can be <jk>null</jk> to unset.
270       * @return This object.
271       */
272      public Input pattern(String value) {
273         pattern = value;
274         return this;
275      }
276
277      /**
278       * Sets the client max-length ({@code text}/{@code textarea} only).
279       *
280       * @param value The max length.  Must be {@code > 0}.
281       * @return This object.
282       */
283      public Input maxLength(int value) {
284         maxLength = value;
285         return this;
286      }
287
288      /**
289       * Sets the help hint (painted with {@code textContent}).
290       *
291       * @param value The hint.  Can be <jk>null</jk> to unset.
292       * @return This object.
293       */
294      public Input help(String value) {
295         help = value;
296         return this;
297      }
298
299      /**
300       * Sets the enclosing view's row-action id this {@code action} button opens.
301       *
302       * @param value The action id.  Can be <jk>null</jk> to unset.
303       * @return This object.
304       */
305      public Input actionId(String value) {
306         actionId = value;
307         return this;
308      }
309
310      /**
311       * Sets the {@code action} target from an {@link ActionRef} (stores {@link ActionRef#id}).
312       *
313       * @param value The action reference.  Must not be <jk>null</jk>.
314       * @return This object.
315       * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
316       */
317      public Input action(ActionRef value) {
318         if (value == null)
319            throw iaex("FormDef.Input action must not be null.");
320         actionId = value.id;
321         return this;
322      }
323
324      /**
325       * Fail-closed per-input validation (called from {@link FormDef#validate()}).
326       *
327       * @throws IllegalArgumentException If this input is not well-formed.
328       */
329      void validate() {
330         if (ib(name))
331            throw iaex("FormDef.Input name must not be null or blank.");
332         var t = resolveType();
333         var isAction = eq(t, "action");
334         var isTextual = eqa(t, "text", "textarea");
335
336         validateOptions(eq(t, "select"));
337         validateTextualOnlyFields(isTextual);
338         validatePattern();
339         validatePasswordValue();
340         validateActionField(isAction);
341      }
342
343      /** Resolves and validates {@link #type} (blank defaults to {@code text}). */
344      private String resolveType() {
345         var t = (ib(type)) ? "text" : type;
346         if (! INPUT_TYPES.contains(t))
347            throw iaex("FormDef.Input '%s' type must be one of text, textarea, password, checkbox, toggle, select, action, not '%s'.", name, t);
348         return t;
349      }
350
351      /** Validates the {@code select}-only {@link #options}/{@link #value} pairing. */
352      private void validateOptions(boolean isSelect) {
353         if (! isSelect) {
354            if (options != null)
355               throw iaex("FormDef.Input '%s' options are only allowed on a select field.", name);
356            return;
357         }
358         if (ie(options))
359            throw iaex("FormDef.Input '%s' select must declare at least one option.", name);
360         var values = new HashSet<String>();
361         for (var o : options) {
362            if (o == null)
363               throw iaex("FormDef.Input '%s' select option must not be null.", name);
364            if (o.value == null)
365               throw iaex("FormDef.Input '%s' select option value must not be null.", name);
366            if (o.label == null || o.label.isBlank())
367               throw iaex("FormDef.Input '%s' select option label must not be null or blank.", name);
368            values.add(o.value);
369         }
370         if (value != null && ! values.contains(value))
371            throw iaex("FormDef.Input '%s' select value '%s' does not match any option.", name, value);
372      }
373
374      /** Validates that {@link #pattern}/{@link #maxLength} are only declared on a textual field. */
375      private void validateTextualOnlyFields(boolean isTextual) {
376         if (isTextual)
377            return;
378         if (pattern != null)
379            throw iaex("FormDef.Input '%s' pattern is only allowed on a text/textarea field.", name);
380         if (maxLength != null)
381            throw iaex("FormDef.Input '%s' maxLength is only allowed on a text/textarea field.", name);
382      }
383
384      /** A {@code password} field is write-only: a {@link #value} would be sent to the client, so it is rejected. */
385      private void validatePasswordValue() {
386         if (eq(type, "password") && value != null)
387            throw iaex("FormDef.Input '%s' value is not allowed on a password field.", name);
388      }
389
390      /** Validates {@link #pattern} compiles and {@link #maxLength} is positive. */
391      private void validatePattern() {
392         if (pattern != null) {
393            if (pattern.length() > PATTERN_MAX_LENGTH)
394               throw iaex("FormDef.Input '%s' pattern must be at most %s characters.", name, PATTERN_MAX_LENGTH);
395            try {
396               Pattern.compile(pattern);
397            } catch (PatternSyntaxException e) {
398               throw iaex("FormDef.Input '%s' pattern does not compile: %s", name, e.getMessage());
399            }
400         }
401         if (maxLength != null && maxLength <= 0)
402            throw iaex("FormDef.Input '%s' maxLength must be > 0.", name);
403      }
404
405      /** Validates the {@code action}-only {@link #actionId}. */
406      private void validateActionField(boolean isAction) {
407         if (isAction && (ib(actionId)))
408            throw iaex("FormDef.Input '%s' action must declare an actionId.", name);
409         if (! isAction && actionId != null)
410            throw iaex("FormDef.Input '%s' actionId is only allowed on an action field.", name);
411      }
412   }
413
414   /**
415    * One labelled group of inputs inside a sectioned form: the multi-page dialog shape.
416    *
417    * <p>
418    * The client paints an ordered list of these as a <b>ribbon</b>-format strip above the form body, one visible pane
419    * at a time.  Switching sections is visibility only &mdash; nothing is refetched and nothing is re-created, which
420    * is why a value typed into one section survives a trip through another, and why an inline validation error stays
421    * attached to the control that owns it rather than to whichever section happens to be showing.
422    *
423    * <p>
424    * A sectioned form is <b>not</b> a stack of dialogs.  The strip opens no layer of its own, so a sectioned dialog
425    * consumes exactly one entry of the runtime's modal-depth budget, the same as a flat one.
426    *
427    * @since 10.0.0
428    */
429   @BeanType(properties="id,label,fields")
430   public static class Section {
431
432      /** The section's stable id, used as the strip tab's identity.  Must not be blank, and must be unique in the form. */
433      public String id;
434
435      /** The visible tab label, painted with {@code textContent}.  Must not be blank. */
436      public String label;
437
438      /** This section's typed inputs in display order.  Must declare at least one. */
439      public List<Input> fields;
440
441      /**
442       * Creates an empty section (add {@link #field(Input) fields}).
443       *
444       * @param id The section's stable id.  Must not be <jk>null</jk> or blank.
445       * @param label The visible tab label.  Must not be <jk>null</jk> or blank.
446       * @return A new {@link Section}.
447       * @throws IllegalArgumentException If {@code id} or {@code label} is <jk>null</jk> or blank.
448       */
449      public static Section of(String id, String label) {
450         if (ib(id))
451            throw iaex("FormDef.Section id must not be null or blank.");
452         if (ib(label))
453            throw iaex("FormDef.Section label must not be null or blank.");
454         var s = new Section();
455         s.id = id;
456         s.label = label;
457         return s;
458      }
459
460      /**
461       * Adds one typed input to this section.
462       *
463       * @param value The input.  Must not be <jk>null</jk>.
464       * @return This object.
465       * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
466       */
467      public Section field(Input value) {
468         if (value == null)
469            throw iaex("FormDef.Section field must not be null.");
470         if (fields == null)
471            fields = l();
472         fields.add(value);
473         return this;
474      }
475
476      /**
477       * Fail-closed per-section validation (called from {@link FormDef#validate()}).
478       *
479       * @throws IllegalArgumentException If this section is not well-formed.
480       */
481      void validate() {
482         if (ib(id))
483            throw iaex("FormDef.Section id must not be null or blank.");
484         if (ib(label))
485            throw iaex("FormDef.Section '%s' label must not be null or blank.", id);
486         if (ie(fields))
487            throw iaex("FormDef.Section '%s' must declare at least one field.", id);
488         for (var f : fields) {
489            if (f == null)
490               throw iaex("FormDef.Section '%s' field must not be null.", id);
491            f.validate();
492         }
493      }
494   }
495
496   /**
497    * The frozen form contract version discriminator.
498    *
499    * <p>
500    * Defaults to {@link #CONTRACT_VERSION} from the instant this form is constructed, independent of
501    * {@link #checked()}.  Guaranteed <b>present</b> is not the same as guaranteed <b>valid</b>: presence
502    * is this field's whole job, structural well-formedness is {@link #validate()}'s (see {@link #checked()}).
503    */
504   public String contractVersion = CONTRACT_VERSION;
505
506   /** Optional FreeMarker template reference for server authors; ignored by the client. */
507   public String template;
508
509   /**
510    * Typed inputs in display order for a flat, single-section form; omitted from the wire when none are declared.
511    *
512    * <p>
513    * Mutually exclusive with {@link #sections} &mdash; a flat {@code fields} list <i>is</i> the single-section case,
514    * so declaring both is an authoring error {@link #validate()} rejects rather than silently picking one.
515    */
516   public List<Input> fields;
517
518   /**
519    * Optional ordered sections for a multi-section form; omitted from the wire when none are declared.
520    *
521    * <p>
522    * Mutually exclusive with {@link #fields} (see that field).  A single-element {@code sections} list is legal but
523    * pointless: it renders a one-tab strip where a flat {@code fields} list would render none.
524    */
525   public List<Section> sections;
526
527   /**
528    * Starts an empty form (add {@link #field(Input) fields} and/or a {@link #template(String) template}).
529    *
530    * @return A new {@link FormDef}.
531    */
532   public static FormDef create() {
533      return new FormDef();
534   }
535
536   /**
537    * Creates a form sourced from the specified FreeMarker template reference.
538    *
539    * @param template The template reference (a resolvable URL/name).  Must not be <jk>null</jk> or blank.
540    * @return A new {@link FormDef}.
541    * @throws IllegalArgumentException If {@code template} is <jk>null</jk> or blank.
542    */
543   public static FormDef ofTemplate(String template) {
544      return create().template(template);
545   }
546
547   /**
548    * Sets the optional server-author template reference.  The client never treats this as HTML.
549    *
550    * @param value The template reference.  Must not be <jk>null</jk> or blank.
551    * @return This object.
552    * @throws IllegalArgumentException If {@code value} is <jk>null</jk> or blank.
553    */
554   public FormDef template(String value) {
555      if (ib(value))
556         throw iaex("FormDef template must not be null or blank.");
557      template = value;
558      return this;
559   }
560
561   /**
562    * Adds one typed input field.
563    *
564    * @param value The input.  Must not be <jk>null</jk>.
565    * @return This object.
566    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
567    */
568   public FormDef field(Input value) {
569      if (value == null)
570         throw iaex("FormDef field must not be null.");
571      if (fields == null)
572         fields = l();
573      fields.add(value);
574      return this;
575   }
576
577   /**
578    * Adds one section, making this a multi-section form.
579    *
580    * @param value The section.  Must not be <jk>null</jk>.
581    * @return This object.
582    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
583    */
584   public FormDef section(Section value) {
585      if (value == null)
586         throw iaex("FormDef section must not be null.");
587      if (sections == null)
588         sections = l();
589      sections.add(value);
590      return this;
591   }
592
593   /**
594    * Fail-closed bean validation.
595    *
596    * <p>
597    * Does <b>not</b> require {@link #contractVersion} to already be set &mdash; a raw-built form validated directly
598    * must not false-refuse on a null version.  A form with neither {@link #fields} nor {@link #sections} is a no-op
599    * (a template-only / fieldless form is a shipped shape).
600    *
601    * <p>
602    * Field names are unique across the <b>whole</b> form, not per section: every control shares one submit body and
603    * one dialog-scoped element-id namespace, so two sections declaring the same name would collide in both.
604    *
605    * @throws IllegalArgumentException If both {@link #fields} and {@link #sections} are declared, if {@link #sections}
606    *    is declared but empty, if two sections share an {@code id}, or if any field is not well-formed or two fields
607    *    share a {@code name}.
608    */
609   @Override
610   public void validate() {
611      if (fields != null && sections != null)
612         throw iaex("FormDef declares both fields and sections; they are mutually exclusive (a flat fields list is the single-section case).");
613      if (sections != null) {
614         validateSections();
615         return;
616      }
617      validateFlatFields();
618   }
619
620   /** Validates the sectioned-form shape: at least one section, unique section ids, unique field names overall. */
621   private void validateSections() {
622      if (sections.isEmpty())
623         throw iaex("FormDef sections must declare at least one section.");
624      var ids = new HashSet<String>();
625      var names = new HashSet<String>();
626      for (var s : sections) {
627         if (s == null)
628            throw iaex("FormDef section must not be null.");
629         s.validate();
630         if (! ids.add(s.id))
631            throw iaex("FormDef duplicate section id '%s'.", s.id);
632         for (var f : s.fields)
633            if (! names.add(f.name))
634               throw iaex("FormDef duplicate field name '%s'.", f.name);
635      }
636   }
637
638   /** Validates the flat-form shape: each field is well-formed, and field names are unique. */
639   private void validateFlatFields() {
640      if (fields == null)
641         return;
642      var names = new HashSet<String>();
643      for (var f : fields) {
644         if (f == null)
645            throw iaex("FormDef field must not be null.");
646         f.validate();
647         if (! names.add(f.name))
648            throw iaex("FormDef duplicate field name '%s'.", f.name);
649      }
650   }
651
652   /**
653    * The serving-path hook: re-stamps {@link #CONTRACT_VERSION} (already set by construction; this
654    * repairs a version a caller explicitly cleared) then {@link #validate() validates}.
655    *
656    * <p>
657    * {@link #contractVersion} no longer depends on this being called. Structural validation
658    * no longer depends on it either for a form served over REST: nested inside a returned {@link ModalDef},
659    * this form is reached by
660    * {@link WidgetsMixin.WidgetValidationProcessor
661    * WidgetsMixin.WidgetValidationProcessor}'s call to {@link ModalDef#validate()}, which recurses into this
662    * bean's own {@link #validate()}, fail-closed with a 500, whether or not the producing endpoint called
663    * {@link ModalDef#checked()}. This method (via {@link ModalDef#checked()}) remains the only fail-closed
664    * structural {@link #validate() validation} a <b>non-REST</b> producer has &mdash; a direct
665    * {@code Json.of(...)} serialization, for instance &mdash; and calling it for a REST-bound form stays safe
666    * (validation is idempotent) even though it is no longer strictly required there.
667    *
668    * @return This object.
669    * @throws IllegalArgumentException If this form is not well-formed.
670    */
671   public FormDef checked() {
672      contractVersion = CONTRACT_VERSION;
673      validate();
674      return this;
675   }
676}