Class FormField<I,​O>

  • Type Parameters:
    I - input value type
    O - output value type

    @Immutable
    public final class FormField<I,​O>
    extends java.lang.Object
    Declarative functional fluent form field converter / validator.

    This class is responsible for converting arbitrary data, sent to us by the web browser, into validated data structures that the server-side code can use. For example:

    
     private enum Gender { MALE, FEMALE }
    
     private static final FormField<String, String> NAME_FIELD = FormField.named("name")
         .matches("[a-z]+")
         .range(atMost(16))
         .required()
         .build();
    
     private static final FormField<String, Gender> GENDER_FIELD = FormField.named("gender")
         .asEnum(Gender.class)
         .required()
         .build();
    
     public Person makePerson(Map<String, String> params) {
       Person.Builder person = new Person.Builder();
       for (String name : NAME_FIELD.extract(params).asSet()) {
         person.setName(name);
       }
       for (Gender name : GENDER_FIELD.extract(params).asSet()) {
         person.setGender(name);
       }
       return person.build();
     }
     

    This class provides full type-safety if and only if you statically initialize your FormField objects and write a unit test that causes the class to be loaded.

    Exception Handling

    When values passed to convert(I) or extract(java.util.Map<java.lang.String, I>) don't meet the contract, FormFieldException will be thrown, which provides the field name and a short error message that's safe to pass along to the client.

    You can safely throw FormFieldException from within your validator functions, and the field name will automatically be propagated into the exception object for you.

    In situations when you're validating lists or maps, you'll end up with a hierarchical field naming structure. For example, if you were validating a list of maps, an error generated by the bar field of the fifth item in the foo field would have a fully-qualified field name of: foo[5][bar].

    Library Definitions

    You should never assign a partially constructed FormField.Builder to a variable or constant. Instead, you should use asBuilder() or asBuilderNamed(String).

    Here is an example of how you might go about defining library definitions:

    
     final class FormFields {
       private static final FormField<String, String> COUNTRY_CODE =
           FormField.named("countryCode")
               .range(Range.singleton(2))
               .uppercased()
               .in(ImmutableSet.copyOf(Locale.getISOCountries()))
               .build();
     }
    
     final class Form {
       private static final FormField<String, String> COUNTRY_CODE_FIELD =
           FormFields.COUNTRY_CODE.asBuilder()
               .required()
               .build();
     }
     
    • Nested Class Summary

      Nested Classes 
      Modifier and Type Class Description
      static class  FormField.Builder<I,​O>
      Mutable builder for FormField.
    • Method Summary

      All Methods Static Methods Instance Methods Concrete Methods 
      Modifier and Type Method Description
      FormField.Builder<I,​O> asBuilder()
      Returns a builder of this object, which can be used to further restrict validation.
      FormField.Builder<I,​O> asBuilderNamed​(java.lang.String newName)
      Same as asBuilder() but changes the field name.
      java.util.Optional<O> convert​(I value)
      Convert and validate a raw user-supplied value.
      java.util.Optional<O> extract​(java.util.Map<java.lang.String,​I> valueMap)
      Convert and validate a raw user-supplied value from a map.
      java.util.Optional<O> extractUntyped​(java.util.Map<java.lang.String,​?> jsonMap)
      Convert and validate a raw user-supplied value from an untyped JSON map.
      static FormField.Builder<java.util.Map<java.lang.String,​?>,​java.util.Map<java.lang.String,​?>> mapNamed​(java.lang.String name)
      Returns a form field builder for validating JSON nested maps.
      java.lang.String name()
      Returns the name of this field.
      static FormField.Builder<java.lang.String,​java.lang.String> named​(java.lang.String name)
      Returns an optional string form field named name.
      static <T> FormField.Builder<T,​T> named​(java.lang.String name, java.lang.Class<T> typeIn)
      Returns an optional form field named name with a specific inputType.
      • Methods inherited from class java.lang.Object

        clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    • Method Detail

      • named

        public static FormField.Builder<java.lang.String,​java.lang.String> named​(java.lang.String name)
        Returns an optional string form field named name.
      • named

        public static <T> FormField.Builder<T,​T> named​(java.lang.String name,
                                                             java.lang.Class<T> typeIn)
        Returns an optional form field named name with a specific inputType.
      • mapNamed

        public static FormField.Builder<java.util.Map<java.lang.String,​?>,​java.util.Map<java.lang.String,​?>> mapNamed​(java.lang.String name)
        Returns a form field builder for validating JSON nested maps.

        Here's an example of how you'd use this feature:

           private static final FormField<String, String> REGISTRAR_NAME_FIELD =
               FormField.named("name")
                   .emptyToNull()
                   .required()
                   .build();
        
           private static final FormField<Map<String, ?>, Registrar> REGISTRAR_FIELD =
               FormField.mapNamed("registrar")
                   .transform(Registrar.class, new Function<Map<String, ?>, Registrar>() {
                     @Nullable
                     @Override
                     public Registrar apply(@Nullable Map<String, ?> params) {
                       Registrar.Builder builder = new Registrar.Builder();
                       for (String name : REGISTRAR_NAME_FIELD.extractUntyped(params).asSet()) {
                        builder.setName(name);
                       }
                       return builder.build();
                     }})
                   .build();

        When a FormFieldException is thrown, it'll be propagated to create a fully-qualified field name. For example, if the JSON input is

        {registrar: {name: ""}}
        then the field name will be registrar.name.
      • name

        public java.lang.String name()
        Returns the name of this field.
      • convert

        @Detainted
        public java.util.Optional<O> convert​(@Tainted @Nullable
                                             I value)
        Convert and validate a raw user-supplied value.
        Throws:
        FormFieldException - if value does not meet expected contracts.
      • extract

        @Detainted
        public java.util.Optional<O> extract​(@Tainted
                                             java.util.Map<java.lang.String,​I> valueMap)
        Convert and validate a raw user-supplied value from a map.

        This is the same as saying: field.convert(valueMap.get(field.name())

        Throws:
        FormFieldException - if value does not meet expected contracts.
      • extractUntyped

        @Detainted
        public java.util.Optional<O> extractUntyped​(@Tainted
                                                    java.util.Map<java.lang.String,​?> jsonMap)
        Convert and validate a raw user-supplied value from an untyped JSON map.
        Throws:
        FormFieldException - if value is wrong type or does not meet expected contracts.