mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 20:17:51 +02:00
The presubmits are warning that toUpperCase() and toLowerCase() are locale-specific, and advise using Ascii.toUpperCase() and Ascii.toLowerCase() as a local-invariant alternative. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=127583677
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.Ascii;
|
|
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 com.google.re2j.Pattern;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
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(Locale.ENGLISH) : 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(Locale.ENGLISH) : 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, Ascii.toUpperCase((String) input)) : 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();
|
|
}
|
|
}
|
|
}
|
|
}
|