mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 20:17:51 +02:00
The dark lord Gosling designed the Java package naming system so that ownership flows from the DNS system. Since we own the domain name registry.google, it seems only appropriate that we should use google.registry as our package name.
790 lines
28 KiB
Java
790 lines
28 KiB
Java
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package google.registry.ui.forms;
|
|
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
import static com.google.common.base.Preconditions.checkState;
|
|
|
|
import com.google.common.base.CharMatcher;
|
|
import com.google.common.base.Function;
|
|
import com.google.common.base.Functions;
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.base.Splitter;
|
|
import com.google.common.collect.FluentIterable;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Range;
|
|
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.regex.Pattern;
|
|
|
|
import javax.annotation.Detainted;
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
import javax.annotation.Tainted;
|
|
import javax.annotation.concurrent.Immutable;
|
|
|
|
/**
|
|
* Declarative functional fluent form field converter / validator.
|
|
*
|
|
* <p>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:<pre>
|
|
*
|
|
* 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();
|
|
* }</pre>
|
|
*
|
|
* <p>This class provides <b>full type-safety</b> <i>if and only if</i> you statically initialize
|
|
* your FormField objects and write a unit test that causes the class to be loaded.
|
|
*
|
|
* <h3>Exception Handling</h3>
|
|
*
|
|
* <p>When values passed to {@link #convert} or {@link #extract} don't meet the contract,
|
|
* {@link FormFieldException} will be thrown, which provides the field name and a short error
|
|
* message that's safe to pass along to the client.
|
|
*
|
|
* <p>You can safely throw {@code FormFieldException} from within your validator functions, and
|
|
* the field name will automatically be propagated into the exception object for you.
|
|
*
|
|
* <p>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
|
|
* {@code bar} field of the fifth item in the {@code foo} field would have a fully-qualified field
|
|
* name of: {@code foo[5][bar]}.
|
|
*
|
|
* <h3>Library Definitions</h3>
|
|
*
|
|
* <p>You should never assign a partially constructed {@code FormField.Builder} to a variable or
|
|
* constant. Instead, you should use {@link #asBuilder()} or {@link #asBuilderNamed(String)}.
|
|
*
|
|
* <p>Here is an example of how you might go about defining library definitions:<pre>
|
|
*
|
|
* 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();
|
|
* }</pre>
|
|
*
|
|
* @param <I> input value type
|
|
* @param <O> output value type
|
|
*/
|
|
@Immutable
|
|
public final class FormField<I, O> {
|
|
|
|
private final String name;
|
|
private final Class<I> typeIn;
|
|
private final Class<O> typeOut;
|
|
private final Function<I, O> converter;
|
|
|
|
private FormField(String name, Class<I> typeIn, Class<O> typeOut, Function<I, O> converter) {
|
|
this.name = name;
|
|
this.typeIn = typeIn;
|
|
this.typeOut = typeOut;
|
|
this.converter = converter;
|
|
}
|
|
|
|
/** Returns an optional string form field named {@code name}. */
|
|
public static Builder<String, String> named(String name) {
|
|
return named(name, String.class);
|
|
}
|
|
|
|
/** Returns an optional form field named {@code name} with a specific {@code inputType}. */
|
|
public static <T> Builder<T, T> named(String name, Class<T> typeIn) {
|
|
checkArgument(!name.isEmpty());
|
|
return new Builder<>(name, checkNotNull(typeIn), typeIn, Functions.<T>identity());
|
|
}
|
|
|
|
/**
|
|
* Returns a form field builder for validating JSON nested maps.
|
|
*
|
|
* <p>Here's an example of how you'd use this feature:
|
|
*
|
|
* <pre>
|
|
* 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();</pre>
|
|
*
|
|
* <p>When a {@link FormFieldException} is thrown, it'll be propagated to create a fully-qualified
|
|
* field name. For example, if the JSON input is <pre>{registrar: {name: ""}}</pre> then the
|
|
* {@link FormFieldException#getFieldName() field name} will be {@code registrar.name}.
|
|
*/
|
|
public static Builder<Map<String, ?>, Map<String, ?>> mapNamed(String name) {
|
|
@SuppressWarnings("unchecked")
|
|
Class<Map<String, ?>> typeIn = (Class<Map<String, ?>>) (Class<?>) Map.class;
|
|
return named(name, typeIn);
|
|
}
|
|
|
|
/** Returns the name of this field. */
|
|
public String name() {
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Convert and validate a raw user-supplied value.
|
|
*
|
|
* @throws FormFieldException if value does not meet expected contracts.
|
|
*/
|
|
@Detainted
|
|
public Optional<O> convert(@Tainted @Nullable I value) {
|
|
try {
|
|
return Optional.fromNullable(converter.apply(value));
|
|
} catch (FormFieldException e) {
|
|
throw e.propagate(name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert and validate a raw user-supplied value from a map.
|
|
*
|
|
* <p>This is the same as saying: {@code field.convert(valueMap.get(field.name())}
|
|
*
|
|
* @throws FormFieldException if value does not meet expected contracts.
|
|
*/
|
|
@Detainted
|
|
public Optional<O> extract(@Tainted Map<String, I> valueMap) {
|
|
return convert(valueMap.get(name));
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@Detainted
|
|
public Optional<O> extractUntyped(@Tainted Map<String, ?> jsonMap) {
|
|
Object value = jsonMap.get(name);
|
|
I castedValue;
|
|
try {
|
|
castedValue = typeIn.cast(value);
|
|
} catch (ClassCastException e) {
|
|
throw new FormFieldException(String.format("Type error: got: %s, expected: %s",
|
|
value.getClass().getSimpleName(),
|
|
typeIn.getSimpleName())).propagate(name);
|
|
}
|
|
return convert(castedValue);
|
|
}
|
|
|
|
/**
|
|
* Returns a builder of this object, which can be used to further restrict validation.
|
|
*
|
|
* @see #asBuilderNamed(String)
|
|
*/
|
|
public Builder<I, O> asBuilder() {
|
|
return new Builder<>(name, typeIn, typeOut, converter);
|
|
}
|
|
|
|
/** Same as {@link #asBuilder()} but changes the field name. */
|
|
public Builder<I, O> asBuilderNamed(String newName) {
|
|
checkArgument(!newName.isEmpty());
|
|
return new Builder<>(newName, typeIn, typeOut, converter);
|
|
}
|
|
|
|
/**
|
|
* Mutable builder for {@link FormField}.
|
|
*
|
|
* @param <I> input value type
|
|
* @param <O> output value type
|
|
*/
|
|
public static final class Builder<I, O> {
|
|
private final String name;
|
|
private final Class<I> typeIn;
|
|
private final Class<O> typeOut;
|
|
private Function<I, O> converter;
|
|
|
|
private Builder(String name, Class<I> typeIn, Class<O> typeOut, Function<I, O> converter) {
|
|
this.name = name;
|
|
this.typeIn = typeIn;
|
|
this.typeOut = typeOut;
|
|
this.converter = converter;
|
|
}
|
|
|
|
/** Causes {@code defaultValue} to be substituted if value is {@code null}. */
|
|
public Builder<I, O> withDefault(O defaultValue) {
|
|
return transform(new DefaultFunction<>(checkNotNull(defaultValue)));
|
|
}
|
|
|
|
/** Ensure value is not {@code null}. */
|
|
public Builder<I, O> required() {
|
|
@SuppressWarnings("unchecked")
|
|
Function<O, O> requiredFunction = (Function<O, O>) REQUIRED_FUNCTION;
|
|
return transform(requiredFunction);
|
|
}
|
|
|
|
/**
|
|
* Transform empty values into {@code null}.
|
|
*
|
|
* @throws IllegalStateException if current output type is not a {@link CharSequence} or
|
|
* {@link Collection}.
|
|
*/
|
|
public Builder<I, O> emptyToNull() {
|
|
checkState(CharSequence.class.isAssignableFrom(typeOut)
|
|
|| Collection.class.isAssignableFrom(typeOut));
|
|
@SuppressWarnings("unchecked")
|
|
Function<O, O> emptyToNullFunction = (Function<O, O>) EMPTY_TO_NULL_FUNCTION;
|
|
return transform(emptyToNullFunction);
|
|
}
|
|
|
|
/**
|
|
* Modify {@link String} input to remove whitespace around the sides.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @throws IllegalStateException if current output type is not a String.
|
|
*/
|
|
public Builder<I, String> trimmed() {
|
|
checkState(String.class.isAssignableFrom(typeOut));
|
|
@SuppressWarnings("unchecked")
|
|
Function<O, String> trimFunction = (Function<O, String>) TRIM_FUNCTION;
|
|
return transform(String.class, trimFunction);
|
|
}
|
|
|
|
/**
|
|
* Modify {@link String} input to be uppercase.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @throws IllegalStateException if current output type is not a String.
|
|
*/
|
|
public Builder<I, String> uppercased() {
|
|
checkState(String.class.isAssignableFrom(typeOut));
|
|
@SuppressWarnings("unchecked")
|
|
Function<O, String> funk = (Function<O, String>) UPPERCASE_FUNCTION;
|
|
return transform(String.class, funk);
|
|
}
|
|
|
|
/**
|
|
* Modify {@link String} input to be lowercase.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @throws IllegalStateException if current output type is not a String.
|
|
*/
|
|
public Builder<I, String> lowercased() {
|
|
checkState(String.class.isAssignableFrom(typeOut));
|
|
@SuppressWarnings("unchecked")
|
|
Function<O, String> funk = (Function<O, String>) LOWERCASE_FUNCTION;
|
|
return transform(String.class, funk);
|
|
}
|
|
|
|
/**
|
|
* Ensure input matches {@code pattern}.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @param pattern is used to validate the user input. It matches against the whole string, so
|
|
* you don't need to use the ^$ characters.
|
|
* @param errorMessage is a helpful error message, which should include an example. If this is
|
|
* not provided, a default error message will be shown that includes the regexp pattern.
|
|
* @throws IllegalStateException if current output type is not a {@link CharSequence}.
|
|
* @see #matches(Pattern)
|
|
*/
|
|
public Builder<I, O> matches(Pattern pattern, @Nullable String errorMessage) {
|
|
checkState(CharSequence.class.isAssignableFrom(typeOut));
|
|
return transform(
|
|
new MatchesFunction<O>(checkNotNull(pattern), Optional.fromNullable(errorMessage)));
|
|
}
|
|
|
|
/** Alias for {@link #matches(Pattern, String) matches(pattern, null)} */
|
|
public Builder<I, O> matches(Pattern pattern) {
|
|
return matches(pattern, null);
|
|
}
|
|
|
|
/**
|
|
* Removes all characters not in {@code matcher}.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @param matcher indicates which characters are to be retained
|
|
* @throws IllegalStateException if current output type is not a {@link CharSequence}
|
|
*/
|
|
public Builder<I, String> retains(CharMatcher matcher) {
|
|
checkState(CharSequence.class.isAssignableFrom(typeOut));
|
|
@SuppressWarnings("unchecked") // safe due to checkState call
|
|
Function<O, String> function =
|
|
(Function<O, String>) new RetainFunction(checkNotNull(matcher));
|
|
return transform(String.class, function);
|
|
}
|
|
|
|
/**
|
|
* Enforce value length/size/value is within {@code range}.
|
|
*
|
|
* <p>The following input value types are supported:
|
|
*
|
|
* <ul>
|
|
* <li>{@link CharSequence}: Length must be within {@code range}.
|
|
* <li>{@link Collection}: Size must be within {@code range}.
|
|
* <li>{@link Number}: Value must be within {@code range}.
|
|
* </ul>
|
|
*
|
|
* <p>{@code null} values are passed through. Please note that setting a lower bound on your
|
|
* range does not imply {@link #required()}, as range checking only applies to non-{@code null}
|
|
* values.
|
|
*
|
|
* @throws IllegalStateException if current output type is not one of the above types.
|
|
*/
|
|
public Builder<I, O> range(Range<Integer> range) {
|
|
checkState(CharSequence.class.isAssignableFrom(typeOut)
|
|
|| Collection.class.isAssignableFrom(typeOut)
|
|
|| Number.class.isAssignableFrom(typeOut));
|
|
return transform(new RangeFunction<O>(checkNotNull(range)));
|
|
}
|
|
|
|
/**
|
|
* Enforce value be a member of {@code values}.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @throws IllegalArgumentException if {@code values} is empty.
|
|
*/
|
|
public Builder<I, O> in(Set<O> values) {
|
|
checkArgument(!values.isEmpty());
|
|
return transform(new InFunction<>(values));
|
|
}
|
|
|
|
/**
|
|
* Performs arbitrary type transformation from {@code O} to {@code T}.
|
|
*
|
|
* <p>Your {@code transform} function is expected to pass-through {@code null} values as a
|
|
* no-op, since it's up to {@link #required()} to block them. You might also want to consider
|
|
* using a try block that rethrows exceptions as {@link FormFieldException}.
|
|
*
|
|
* <p>Here's an example of how you'd convert from String to Integer:
|
|
*
|
|
* <pre>
|
|
* FormField.named("foo", String.class)
|
|
* .transform(Integer.class, new Function<String, Integer>() {
|
|
* @Nullable
|
|
* @Override
|
|
* public Integer apply(@Nullable String input) {
|
|
* try {
|
|
* return input != null ? Integer.parseInt(input) : null;
|
|
* } catch (IllegalArgumentException e) {
|
|
* throw new FormFieldException("Invalid number.", e);
|
|
* }
|
|
* }})
|
|
* .build();</pre>
|
|
*
|
|
* @see #transform(Function)
|
|
*/
|
|
public <T> Builder<I, T> transform(Class<T> newType, Function<O, T> transform) {
|
|
return new Builder<>(name, typeIn, checkNotNull(newType),
|
|
Functions.compose(checkNotNull(transform), converter));
|
|
}
|
|
|
|
/**
|
|
* Manipulates values without changing type.
|
|
*
|
|
* <p>Please see {@link #transform(Class, Function)} for information about the contract to
|
|
* which {@code transform} is expected to conform.
|
|
*/
|
|
public Builder<I, O> transform(Function<O, O> transform) {
|
|
this.converter = Functions.compose(checkNotNull(transform), converter);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Uppercases value and converts to an enum field of {@code enumClass}.
|
|
*
|
|
* <p>{@code null} values are passed through.
|
|
*
|
|
* @throws IllegalArgumentException if {@code enumClass} is not an enum class.
|
|
* @throws IllegalStateException if current output type is not a String.
|
|
*/
|
|
public <C extends Enum<C>> Builder<I, C> asEnum(Class<C> enumClass) {
|
|
checkArgument(enumClass.isEnum());
|
|
checkState(String.class.isAssignableFrom(typeOut));
|
|
return transform(enumClass, new ToEnumFunction<O, C>(enumClass));
|
|
}
|
|
|
|
/**
|
|
* Turns this form field into something that processes lists.
|
|
*
|
|
* <p>The current object definition will be applied to each item in the list. If a
|
|
* {@link FormFieldException} is thrown when processing an item, then its
|
|
* {@link FormFieldException#getFieldName() fieldName} will be rewritten to include the index,
|
|
* e.g. {@code name} becomes {@code name[0]}.
|
|
*
|
|
* <p>The outputted list will be an {@link ImmutableList}. This is not reflected in the generic
|
|
* typing for the sake of brevity.
|
|
*
|
|
* <p>A {@code null} value for list will be passed through. List items that convert to
|
|
* {@code null} will be discarded (since {@code ImmutableList} does not permit {@code null}
|
|
* values).
|
|
*/
|
|
public Builder<List<I>, List<O>> asList() {
|
|
@SuppressWarnings("unchecked") Class<List<I>> in = (Class<List<I>>) (Class<I>) List.class;
|
|
@SuppressWarnings("unchecked") Class<List<O>> out = (Class<List<O>>) (Class<O>) List.class;
|
|
return new Builder<>(name, in, out, new ToListFunction<>(build()));
|
|
}
|
|
|
|
/**
|
|
* Turns this form field into a split string list that applies itself to each item.
|
|
*
|
|
* <p>The behavior of this method is counter-intuitive. It behaves similar to {@link #asList()}
|
|
* in the sense that all transforms specified <i>before</i> this method will be applied to the
|
|
* individual resulting list items.
|
|
*
|
|
* <p>For example, to turn a comma-delimited string into an enum list:<pre> {@code
|
|
*
|
|
* private static final FormField<String, List<State>> STATES_FIELD =
|
|
* FormField.named("states")
|
|
* .uppercased()
|
|
* .asEnum(State.class)
|
|
* .asList(Splitter.on(',').omitEmptyStrings().trimResults())
|
|
* .build();}</pre>
|
|
*
|
|
* <p>You'll notice that the transforms specified before this method are applied to each list
|
|
* item. However unlike {@link #asList()}, if an error is thrown on an individual item, then
|
|
* {@link FormFieldException#getFieldName()} will <i>not</i> contain the index.
|
|
*
|
|
* @throws IllegalStateException If either the current input type isn't String.
|
|
*/
|
|
public Builder<String, List<O>> asList(Splitter splitter) {
|
|
checkNotNull(splitter);
|
|
checkState(String.class.isAssignableFrom(typeIn));
|
|
@SuppressWarnings("unchecked") Class<List<O>> out = (Class<List<O>>) (Class<O>) List.class;
|
|
@SuppressWarnings("unchecked") FormField<String, O> inField = (FormField<String, O>) build();
|
|
return new Builder<>(name, String.class, out, new SplitToListFunction<>(inField, splitter));
|
|
}
|
|
|
|
/**
|
|
* Same as {@link #asList()} but outputs an {@link ImmutableSet} instead.
|
|
*
|
|
* @throws IllegalStateException if you called asList() before calling this method.
|
|
*/
|
|
public Builder<List<I>, Set<O>> asSet() {
|
|
checkState(!List.class.isAssignableFrom(typeOut));
|
|
@SuppressWarnings("unchecked")
|
|
Class<Set<O>> setOut = (Class<Set<O>>) (Class<O>) Set.class;
|
|
@SuppressWarnings("unchecked")
|
|
Function<List<O>, Set<O>> toSetFunction =
|
|
(Function<List<O>, Set<O>>) (Function<O, O>) TO_SET_FUNCTION;
|
|
return asList().transform(setOut, toSetFunction);
|
|
}
|
|
|
|
/**
|
|
* Same as {@link #asList(Splitter)} but outputs an {@link ImmutableSet} instead.
|
|
*
|
|
* @throws IllegalStateException If the current input type isn't String.
|
|
*/
|
|
public Builder<String, Set<O>> asSet(Splitter splitter) {
|
|
checkNotNull(splitter);
|
|
checkState(String.class.isAssignableFrom(typeIn));
|
|
@SuppressWarnings("unchecked") Class<Set<O>> out = (Class<Set<O>>) (Class<O>) Set.class;
|
|
@SuppressWarnings("unchecked") FormField<String, O> inField = (FormField<String, O>) build();
|
|
return new Builder<>(name, String.class, out, new SplitToSetFunction<>(inField, splitter));
|
|
}
|
|
|
|
/** Creates a new {@link FormField} instance. */
|
|
public FormField<I, O> build() {
|
|
return new FormField<>(name, typeIn, typeOut, converter);
|
|
}
|
|
|
|
private static final Function<List<Object>, Set<Object>> TO_SET_FUNCTION =
|
|
new Function<List<Object>, Set<Object>>() {
|
|
@Nullable
|
|
@Override
|
|
public Set<Object> apply(@Nullable List<Object> input) {
|
|
return input != null ? ImmutableSet.copyOf(input) : null;
|
|
}};
|
|
|
|
private static final Function<String, String> TRIM_FUNCTION =
|
|
new Function<String, String>() {
|
|
@Nullable
|
|
@Override
|
|
public String apply(@Nullable String input) {
|
|
return input != null ? input.trim() : null;
|
|
}};
|
|
|
|
private static final Function<String, String> UPPERCASE_FUNCTION =
|
|
new Function<String, String>() {
|
|
@Nullable
|
|
@Override
|
|
public String apply(@Nullable String input) {
|
|
return input != null ? input.toUpperCase() : null;
|
|
}};
|
|
|
|
private static final Function<String, String> LOWERCASE_FUNCTION =
|
|
new Function<String, String>() {
|
|
@Nullable
|
|
@Override
|
|
public String apply(@Nullable String input) {
|
|
return input != null ? input.toLowerCase() : null;
|
|
}};
|
|
|
|
private static final Function<Object, Object> REQUIRED_FUNCTION =
|
|
new Function<Object, Object>() {
|
|
@Nonnull
|
|
@Override
|
|
public Object apply(@Nullable Object input) {
|
|
if (input == null) {
|
|
throw new FormFieldException("This field is required.");
|
|
}
|
|
return input;
|
|
}};
|
|
|
|
private static final Function<Object, Object> EMPTY_TO_NULL_FUNCTION =
|
|
new Function<Object, Object>() {
|
|
@Nullable
|
|
@Override
|
|
public Object apply(@Nullable Object input) {
|
|
return input instanceof CharSequence && ((CharSequence) input).length() == 0
|
|
|| input instanceof Collection && ((Collection<?>) input).isEmpty()
|
|
? null : input;
|
|
}};
|
|
|
|
private static final class DefaultFunction<O> implements Function<O, O> {
|
|
private final O defaultValue;
|
|
|
|
DefaultFunction(O defaultValue) {
|
|
this.defaultValue = defaultValue;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public O apply(@Nullable O input) {
|
|
return input != null ? input : defaultValue;
|
|
}
|
|
}
|
|
|
|
private static final class RangeFunction<O> implements Function<O, O> {
|
|
private final Range<Integer> range;
|
|
|
|
RangeFunction(Range<Integer> range) {
|
|
this.range = range;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public O apply(@Nullable O input) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
if (input instanceof CharSequence) {
|
|
checkRangeContains(range, ((CharSequence) input).length(), "Number of characters");
|
|
} else if (input instanceof Collection) {
|
|
checkRangeContains(range, ((Collection<?>) input).size(), "Number of items");
|
|
} else if (input instanceof Number) {
|
|
checkRangeContains(range, ((Number) input).intValue(), "Value");
|
|
} else {
|
|
throw new AssertionError();
|
|
}
|
|
return input;
|
|
}
|
|
|
|
private void checkRangeContains(Range<Integer> range, int value, String message) {
|
|
if (!range.contains(value)) {
|
|
throw new FormFieldException(
|
|
String.format("%s (%,d) not in range %s", message, value, range));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final class InFunction<O> implements Function<O, O> {
|
|
private final Set<O> values;
|
|
|
|
InFunction(Set<O> values) {
|
|
this.values = values;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public O apply(@Nullable O input) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
if (!values.contains(input)) {
|
|
throw new FormFieldException("Unrecognized value.");
|
|
}
|
|
return input;
|
|
}
|
|
}
|
|
|
|
private static final class MatchesFunction<O> implements Function<O, O> {
|
|
private final Pattern pattern;
|
|
private final Optional<String> errorMessage;
|
|
|
|
MatchesFunction(Pattern pattern, Optional<String> errorMessage) {
|
|
this.pattern = pattern;
|
|
this.errorMessage = errorMessage;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public O apply(@Nullable O input) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
if (!pattern.matcher((CharSequence) input).matches()) {
|
|
throw new FormFieldException(errorMessage.or("Must match pattern: " + pattern));
|
|
}
|
|
return input;
|
|
}
|
|
}
|
|
|
|
private static final class RetainFunction implements Function<CharSequence, String> {
|
|
private final CharMatcher matcher;
|
|
|
|
RetainFunction(CharMatcher matcher) {
|
|
this.matcher = matcher;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public String apply(@Nullable CharSequence input) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
return matcher.retainFrom(input);
|
|
}
|
|
}
|
|
|
|
private static final class ToEnumFunction<O, C extends Enum<C>> implements Function<O, C> {
|
|
private final Class<C> enumClass;
|
|
|
|
ToEnumFunction(Class<C> enumClass) {
|
|
this.enumClass = enumClass;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public C apply(@Nullable O input) {
|
|
try {
|
|
return input != null ? Enum.valueOf(enumClass, ((String) input).toUpperCase()) : null;
|
|
} catch (IllegalArgumentException e) {
|
|
throw new FormFieldException(
|
|
String.format("Enum %s does not contain '%s'", enumClass.getSimpleName(), input));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final class ToListFunction<I, O> implements Function<List<I>, List<O>> {
|
|
private final FormField<I, O> itemField;
|
|
|
|
ToListFunction(FormField<I, O> itemField) {
|
|
this.itemField = itemField;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public List<O> apply(@Nullable List<I> input) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
ImmutableList.Builder<O> builder = new ImmutableList.Builder<>();
|
|
for (int i = 0; i < input.size(); i++) {
|
|
I inputItem = itemField.typeIn.cast(input.get(i));
|
|
O outputItem;
|
|
try {
|
|
outputItem = itemField.converter.apply(inputItem);
|
|
} catch (FormFieldException e) {
|
|
throw e.propagate(i);
|
|
}
|
|
if (outputItem != null) {
|
|
builder.add(outputItem);
|
|
}
|
|
}
|
|
return builder.build();
|
|
}
|
|
}
|
|
|
|
private static final class SplitToListFunction<O> implements Function<String, List<O>> {
|
|
private final FormField<String, O> itemField;
|
|
private final Splitter splitter;
|
|
|
|
SplitToListFunction(FormField<String, O> itemField, Splitter splitter) {
|
|
this.itemField = itemField;
|
|
this.splitter = splitter;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public List<O> apply(@Nullable String input) {
|
|
return input == null ? null : FluentIterable
|
|
.from(splitter.split(input))
|
|
.transform(itemField.converter)
|
|
.toList();
|
|
}
|
|
}
|
|
|
|
private static final class SplitToSetFunction<O> implements Function<String, Set<O>> {
|
|
private final FormField<String, O> itemField;
|
|
private final Splitter splitter;
|
|
|
|
SplitToSetFunction(FormField<String, O> itemField, Splitter splitter) {
|
|
this.itemField = itemField;
|
|
this.splitter = splitter;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public Set<O> apply(@Nullable String input) {
|
|
return input == null ? null : FluentIterable
|
|
.from(splitter.split(input))
|
|
.transform(itemField.converter)
|
|
.toSet();
|
|
}
|
|
}
|
|
}
|
|
}
|