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 — 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 — 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 — 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} — 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 — 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 — a direct 665 * {@code Json.of(...)} serialization, for instance — 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}