mirror of
https://github.com/google/nomulus
synced 2025-12-23 14:25:44 +00:00
Remove more old-console-related files (#2866)
This commit is contained in:
@@ -49,7 +49,6 @@ import google.registry.request.Modules.UrlConnectionServiceModule;
|
||||
import google.registry.request.RequestHandler;
|
||||
import google.registry.request.auth.AuthModule;
|
||||
import google.registry.request.auth.RequestAuthenticator;
|
||||
import google.registry.ui.ConsoleDebug.ConsoleConfigModule;
|
||||
import google.registry.util.UtilsModule;
|
||||
import jakarta.inject.Provider;
|
||||
import jakarta.inject.Singleton;
|
||||
@@ -63,7 +62,6 @@ import jakarta.inject.Singleton;
|
||||
BigqueryModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
ConfigModule.class,
|
||||
ConsoleConfigModule.class,
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
DirectoryModule.class,
|
||||
|
||||
@@ -35,7 +35,6 @@ import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.request.Modules.GsonModule;
|
||||
import google.registry.request.Modules.NetHttpTransportModule;
|
||||
import google.registry.request.auth.AuthModule;
|
||||
import google.registry.ui.ConsoleDebug.ConsoleConfigModule;
|
||||
import google.registry.util.UtilsModule;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -46,7 +45,6 @@ import jakarta.inject.Singleton;
|
||||
AuthModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
ConfigModule.class,
|
||||
ConsoleConfigModule.class,
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
/** Enum defining which JS/CSS files get rendered in a soy templates. */
|
||||
public enum ConsoleDebug {
|
||||
|
||||
/** Use compiled CSS and JS. */
|
||||
PRODUCTION,
|
||||
|
||||
/** Use debug compiled CSS and JS, where symbols are only <i>slightly</i> mangled. */
|
||||
DEBUG,
|
||||
|
||||
/**
|
||||
* Use debug compiled CSS and raw JS from internal source code dependency-managed directory
|
||||
* structure.
|
||||
*/
|
||||
RAW,
|
||||
|
||||
/** Don't use any CSS or JS. This is used by JSTD unit tests. */
|
||||
TEST;
|
||||
|
||||
private static final String PROPERTY = "console.debug";
|
||||
private static final String DEFAULT = PRODUCTION.name();
|
||||
|
||||
/** Returns value configured by system property {@code #PROPERTY}. */
|
||||
public static ConsoleDebug get() {
|
||||
return valueOf(System.getProperty(PROPERTY, DEFAULT));
|
||||
}
|
||||
|
||||
/** Sets the global {@link ConsoleDebug} state. */
|
||||
public static void set(ConsoleDebug value) {
|
||||
System.setProperty(PROPERTY, value.toString());
|
||||
}
|
||||
|
||||
/** Dagger module for ConsoleDebug. */
|
||||
@Module
|
||||
public static final class ConsoleConfigModule {
|
||||
|
||||
@Provides
|
||||
static ConsoleDebug provideConsoleDebug() {
|
||||
return ConsoleDebug.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.checkNotNull;
|
||||
|
||||
import javax.annotation.Detainted;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
/**
|
||||
* Exception thrown when a form is invalid. Problems with a specific
|
||||
* form field should use {@link FormFieldException} instead.
|
||||
*
|
||||
* <p>You can safely throw {@code FormException} from within your form
|
||||
* validator, and the message will automatically be propagated to the
|
||||
* client form interface.
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public class FormException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormException}
|
||||
*
|
||||
* @param userMessage should be a friendly message that's safe to show to the user.
|
||||
*/
|
||||
public FormException(@Detainted String userMessage) {
|
||||
super(checkNotNull(userMessage, "userMessage"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormException}
|
||||
*
|
||||
* @param userMessage should be a friendly message that's safe to show to the user.
|
||||
* @param cause the original cause of this exception. May be null.
|
||||
*/
|
||||
public FormException(@Detainted String userMessage, Throwable cause) {
|
||||
super(checkNotNull(userMessage, "userMessage"), cause);
|
||||
}
|
||||
|
||||
/** Returns an error message that's safe to display to the user. */
|
||||
@Override
|
||||
@Detainted
|
||||
public String getMessage() {
|
||||
return super.getMessage();
|
||||
}
|
||||
}
|
||||
@@ -1,762 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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 static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.re2j.Pattern;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Detainted;
|
||||
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>{@code
|
||||
* 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.
|
||||
*
|
||||
* <h2>Exception Handling</h2>
|
||||
*
|
||||
* <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>{@code
|
||||
* 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, x -> x);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.ofNullable(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() {
|
||||
return transform(Builder::checkNotNullTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
return transform(
|
||||
input ->
|
||||
((input instanceof CharSequence) && (((CharSequence) input).length() == 0))
|
||||
|| ((input instanceof Collection) && ((Collection<?>) input).isEmpty())
|
||||
? null
|
||||
: input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>)
|
||||
((Function<String, String>) input -> input != null ? input.trim() : null);
|
||||
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>)
|
||||
((Function<String, String>)
|
||||
input -> input != null ? input.toUpperCase(Locale.ENGLISH) : null);
|
||||
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>)
|
||||
((Function<String, String>)
|
||||
input -> input != null ? input.toLowerCase(Locale.ENGLISH) : null);
|
||||
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<>(checkNotNull(pattern), Optional.ofNullable(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<>(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), this.converter.andThen(checkNotNull(transform)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = this.converter.andThen(checkNotNull(transform));
|
||||
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<>(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>)
|
||||
((Function<List<Object>, Set<Object>>)
|
||||
input -> input != null ? ImmutableSet.copyOf(input) : null);
|
||||
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 <O> O checkNotNullTransform(@Nullable O input) {
|
||||
if (input == null) {
|
||||
throw new FormFieldException("This field is required.");
|
||||
}
|
||||
return 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.orElse("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
|
||||
: Streams.stream(splitter.split(input))
|
||||
.map(itemField.converter)
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
: Streams.stream(splitter.split(input))
|
||||
.map(itemField.converter)
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.MoreObjects.toStringHelper;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Detainted;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
/**
|
||||
* Exception thrown when a form field contains a bad value.
|
||||
*
|
||||
* <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>The way that field names work is a bit complicated, because we need to support complex nested
|
||||
* field names like {@code foo[3].bar}. So what happens is the original exception will be thrown by
|
||||
* a {@link FormField} validator without the field set. Then as the exception bubbles up the stack,
|
||||
* it'll be caught by the {@link FormField#convert(Object) convert} method, which then prepends the
|
||||
* name of that component. Then when the exception reaches the user, the {@link #getFieldName()}
|
||||
* method will produce the fully-qualified field name.
|
||||
*
|
||||
* <p>This propagation mechanism is also very important when writing {@link
|
||||
* FormField.Builder#transform} functions, which oftentimes will not know the name of the field
|
||||
* they're validating.
|
||||
*/
|
||||
@NotThreadSafe
|
||||
@SuppressWarnings("OverrideThrowableToString")
|
||||
public final class FormFieldException extends FormException {
|
||||
|
||||
private final List<Object> names = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
private String lazyFieldName;
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormFieldException}
|
||||
*
|
||||
* <p>This exception should only be thrown from within a {@link FormField} converter function.
|
||||
* The field name will automatically be propagated into the exception object for you.
|
||||
*
|
||||
* @param userMessage should be a friendly message that's safe to show to the user.
|
||||
*/
|
||||
public FormFieldException(@Detainted String userMessage) {
|
||||
super(checkNotNull(userMessage, "userMessage"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormFieldException}
|
||||
*
|
||||
* <p>This exception should only be thrown from within a {@link FormField} converter function.
|
||||
* The field name will automatically be propagated into the exception object for you.
|
||||
*
|
||||
* @param userMessage should be a friendly message that's safe to show to the user.
|
||||
* @param cause the original cause of this exception (non-null).
|
||||
*/
|
||||
public FormFieldException(@Detainted String userMessage, Throwable cause) {
|
||||
super(checkNotNull(userMessage, "userMessage"), checkNotNull(cause, "cause"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormFieldException} for a particular form field.
|
||||
*
|
||||
* <p>This exception should only be thrown from within a {@link FormField} MAP converter function
|
||||
* in situations where you're performing additional manual validation.
|
||||
*
|
||||
* @param userMessage should be a friendly message that's safe to show to the user.
|
||||
*/
|
||||
public FormFieldException(FormField<?, ?> field, @Detainted String userMessage) {
|
||||
this(field.name(), userMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormFieldException} for a particular field name.
|
||||
*
|
||||
* @param field name corresponding to a {@link FormField#name()}
|
||||
* @param userMessage friendly message that's safe to show to the user
|
||||
*/
|
||||
public FormFieldException(String field, @Detainted String userMessage) {
|
||||
super(checkNotNull(userMessage, "userMessage"), null);
|
||||
propagateImpl(field);
|
||||
}
|
||||
|
||||
/** Returns the fully-qualified name (JavaScript syntax) of the form field causing this error. */
|
||||
public String getFieldName() {
|
||||
String fieldName = lazyFieldName;
|
||||
if (fieldName == null) {
|
||||
lazyFieldName = fieldName = getFieldNameImpl();
|
||||
}
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
private String getFieldNameImpl() {
|
||||
checkState(!names.isEmpty(),
|
||||
"FormFieldException was thrown outside FormField infrastructure!");
|
||||
Iterator<Object> namesIterator = Lists.reverse(names).iterator();
|
||||
StringBuilder result = new StringBuilder((String) namesIterator.next());
|
||||
while (namesIterator.hasNext()) {
|
||||
Object name = namesIterator.next();
|
||||
if (name instanceof String) {
|
||||
result.append('.').append(name);
|
||||
} else if (name instanceof Integer) {
|
||||
result.append('[').append(name).append(']');
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns self with {@code name} prepended, for propagating exceptions up the stack.
|
||||
*
|
||||
* <p>This would be package-private except that it needs to be called by a test class in another
|
||||
* package.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public FormFieldException propagate(String name) {
|
||||
return propagateImpl(name);
|
||||
}
|
||||
|
||||
/** Returns self with {@code index} prepended, for propagating exceptions up the stack. */
|
||||
@CheckReturnValue
|
||||
FormFieldException propagate(int index) {
|
||||
return propagateImpl(index);
|
||||
}
|
||||
|
||||
/** Returns self with {@code name} prepended, for propagating exceptions up the stack. */
|
||||
private FormFieldException propagateImpl(Object name) {
|
||||
lazyFieldName = null;
|
||||
names.add(checkNotNull(name));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return this == obj
|
||||
|| (obj instanceof FormFieldException
|
||||
&& Objects.equals(getCause(), ((FormFieldException) obj).getCause())
|
||||
&& Objects.equals(getMessage(), ((FormFieldException) obj).getMessage())
|
||||
&& Objects.equals(names, ((FormFieldException) obj).names));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getCause(), getMessage(), getFieldName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(getClass())
|
||||
.add("fieldName", getFieldName())
|
||||
.add("message", getMessage())
|
||||
.add("cause", getCause())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.collect.Range.atMost;
|
||||
import static com.google.common.collect.Range.closed;
|
||||
import static com.google.common.collect.Range.singleton;
|
||||
import static java.util.Locale.getISOCountries;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.re2j.Pattern;
|
||||
|
||||
/** Utility class of {@link FormField} objects for validating EPP related things. */
|
||||
public final class FormFields {
|
||||
|
||||
private static final Pattern WHITESPACE = Pattern.compile("[ \\t\\r\\n]+");
|
||||
/**
|
||||
* Form field that applies XML Schema Token cleanup to input.
|
||||
*
|
||||
* <p>This trims the input and collapses whitespace.
|
||||
*
|
||||
* @see <a href="http://www.w3.org/TR/xmlschema11-2/#token">XSD Datatypes - token</a>
|
||||
*/
|
||||
public static final FormField<String, String> XS_TOKEN =
|
||||
FormField.named("xsToken")
|
||||
.emptyToNull()
|
||||
.trimmed()
|
||||
.transform(input -> input != null ? WHITESPACE.matcher(input).replaceAll(" ") : null)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Form field that ensures input does not contain tabs, line feeds, or carriage returns.
|
||||
*
|
||||
* @see <a href="http://www.w3.org/TR/xmlschema11-2/#normalizedString">
|
||||
* XSD Datatypes - normalizedString</a>
|
||||
*/
|
||||
public static final FormField<String, String> XS_NORMALIZED_STRING =
|
||||
FormField.named("xsNormalizedString")
|
||||
.emptyToNull()
|
||||
.matches(Pattern.compile("[^\\t\\r\\n]*"), "Must not contain tabs or multiple lines.")
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Form field for +E164 phone numbers with a dot after the country prefix.
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc5733#section-4">RFC 5733 - EPP - Formal Syntax</a>
|
||||
*/
|
||||
public static final FormField<String, String> PHONE_NUMBER =
|
||||
XS_TOKEN.asBuilderNamed("phoneNumber")
|
||||
.range(atMost(17))
|
||||
.matches(Pattern.compile("(\\+[0-9]{1,3}\\.[0-9]{1,14})?"),
|
||||
"Must be a valid +E.164 phone number, e.g. +1.2125650000")
|
||||
.build();
|
||||
|
||||
/** Form field for EPP client identifiers. */
|
||||
public static final FormField<String, String> CLID = XS_TOKEN.asBuilderNamed("clid")
|
||||
.range(closed(3, 16))
|
||||
.build();
|
||||
|
||||
/** Form field for passwords (see pwType in epp.xsd). */
|
||||
public static final FormField<String, String> PASSWORD = XS_TOKEN.asBuilderNamed("password")
|
||||
.range(closed(6, 16))
|
||||
.build();
|
||||
|
||||
/** Form field for non-empty tokens (see minToken in eppcom.xsd). */
|
||||
public static final FormField<String, String> MIN_TOKEN = XS_TOKEN.asBuilderNamed("minToken")
|
||||
.emptyToNull()
|
||||
.build();
|
||||
|
||||
/** Form field for nameType (see rde-registrar/notification). */
|
||||
public static final FormField<String, String> NAME = XS_NORMALIZED_STRING.asBuilderNamed("name")
|
||||
.range(closed(1, 255))
|
||||
.build();
|
||||
|
||||
/** Form field for {@code labelType} from {@code eppcom.xsd}. */
|
||||
public static final FormField<String, String> LABEL = XS_TOKEN.asBuilderNamed("label")
|
||||
.range(closed(1, 255))
|
||||
.build();
|
||||
|
||||
/** Email address form field. */
|
||||
public static final FormField<String, String> EMAIL = XS_TOKEN.asBuilderNamed("email")
|
||||
.matches(Pattern.compile("[^@]+@[^@.]+\\.[^@]+"), "Please enter a valid email address.")
|
||||
.build();
|
||||
|
||||
/** Two-letter ISO country code form field. */
|
||||
public static final FormField<String, String> COUNTRY_CODE =
|
||||
XS_TOKEN.asBuilderNamed("countryCode")
|
||||
.range(singleton(2))
|
||||
.uppercased()
|
||||
.in(ImmutableSet.copyOf(getISOCountries()))
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Ensure value is an EPP Repository Object IDentifier (ROID).
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc5730#section-4.2">Shared Structure Schema</a>
|
||||
*/
|
||||
public static final FormField<String, String> ROID = XS_TOKEN.asBuilderNamed("roid")
|
||||
.matches(Pattern.compile("(\\w|_){1,80}-\\w{1,8}"),
|
||||
"Please enter a valid EPP ROID, e.g. SH8013-REP")
|
||||
.build();
|
||||
|
||||
private FormFields() {}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.
|
||||
|
||||
/** Web application backend form processing utilities. */
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.ui.forms;
|
||||
@@ -1,15 +0,0 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Server Error</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}
|
||||
html,code{font:15px/22px arial,sans-serif}
|
||||
html{background:#fff;color:#222;padding:15px}
|
||||
body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}
|
||||
* > body{background:url(/manager/img/ui/robot.png) 100% 5px no-repeat;padding-right:205px}
|
||||
p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}
|
||||
a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}
|
||||
</style>
|
||||
<a href="/"><img src="/assets/images/logo_sm.gif" alt=Google></a>
|
||||
<p><b>500.</b> <ins>That's an error.</ins>
|
||||
<p>Sorry, but the server encountered an error while processing your request.
|
||||
@@ -1,6 +0,0 @@
|
||||
<!doctype html>
|
||||
<meta http-equiv="refresh" content="0;URL=/console">
|
||||
<title>Nomulus</title>
|
||||
<body lang="en-US">
|
||||
If this page doesn't change automatically, please go
|
||||
to <a href="/console">https://www.registry.google/console</a>
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.ui;
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.server;
|
||||
|
||||
import com.google.common.collect.ImmutableBiMap;
|
||||
|
||||
/**
|
||||
* Bimap of state codes and names for the US Regime.
|
||||
*
|
||||
* @see <a href="http://statetable.com/">State Table</a>
|
||||
*/
|
||||
public final class StateCode {
|
||||
|
||||
public static final ImmutableBiMap<String, String> US_MAP =
|
||||
new ImmutableBiMap.Builder<String, String>()
|
||||
.put("AL", "Alabama")
|
||||
.put("AK", "Alaska")
|
||||
.put("AZ", "Arizona")
|
||||
.put("AR", "Arkansas")
|
||||
.put("CA", "California")
|
||||
.put("CO", "Colorado")
|
||||
.put("CT", "Connecticut")
|
||||
.put("DE", "Delaware")
|
||||
.put("FL", "Florida")
|
||||
.put("GA", "Georgia")
|
||||
.put("HI", "Hawaii")
|
||||
.put("ID", "Idaho")
|
||||
.put("IL", "Illinois")
|
||||
.put("IN", "Indiana")
|
||||
.put("IA", "Iowa")
|
||||
.put("KS", "Kansas")
|
||||
.put("KY", "Kentucky")
|
||||
.put("LA", "Louisiana")
|
||||
.put("ME", "Maine")
|
||||
.put("MD", "Maryland")
|
||||
.put("MA", "Massachusetts")
|
||||
.put("MI", "Michigan")
|
||||
.put("MN", "Minnesota")
|
||||
.put("MS", "Mississippi")
|
||||
.put("MO", "Missouri")
|
||||
.put("MT", "Montana")
|
||||
.put("NE", "Nebraska")
|
||||
.put("NV", "Nevada")
|
||||
.put("NH", "New Hampshire")
|
||||
.put("NJ", "New Jersey")
|
||||
.put("NM", "New Mexico")
|
||||
.put("NY", "New York")
|
||||
.put("NC", "North Carolina")
|
||||
.put("ND", "North Dakota")
|
||||
.put("OH", "Ohio")
|
||||
.put("OK", "Oklahoma")
|
||||
.put("OR", "Oregon")
|
||||
.put("PA", "Pennsylvania")
|
||||
.put("RI", "Rhode Island")
|
||||
.put("SC", "South Carolina")
|
||||
.put("SD", "South Dakota")
|
||||
.put("TN", "Tennessee")
|
||||
.put("TX", "Texas")
|
||||
.put("UT", "Utah")
|
||||
.put("VT", "Vermont")
|
||||
.put("VA", "Virginia")
|
||||
.put("WA", "Washington")
|
||||
.put("WV", "West Virginia")
|
||||
.put("WI", "Wisconsin")
|
||||
.put("WY", "Wyoming")
|
||||
.put("DC", "Washington DC")
|
||||
.build();
|
||||
|
||||
private StateCode() {}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ import google.registry.request.Action.GaeService;
|
||||
import google.registry.request.Action.GkeService;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.ui.forms.FormException;
|
||||
import google.registry.ui.server.console.ConsoleApiAction;
|
||||
import google.registry.ui.server.console.ConsoleApiParams;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -166,7 +165,7 @@ public class ContactAction extends ConsoleApiAction {
|
||||
|
||||
try {
|
||||
checkContactRequirements(oldContacts, newContacts);
|
||||
} catch (FormException e) {
|
||||
} catch (ContactRequirementException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Error processing contacts post request for registrar: %s", registrarId);
|
||||
throw new IllegalArgumentException(e);
|
||||
@@ -196,7 +195,7 @@ public class ContactAction extends ConsoleApiAction {
|
||||
/**
|
||||
* Enforces business logic checks on registrar contacts.
|
||||
*
|
||||
* @throws FormException if the checks fail.
|
||||
* @throws ContactRequirementException if the checks fail.
|
||||
*/
|
||||
private static void checkContactRequirements(
|
||||
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
|
||||
@@ -299,7 +298,7 @@ public class ContactAction extends ConsoleApiAction {
|
||||
}
|
||||
|
||||
/** Thrown when a set of contacts doesn't meet certain constraints. */
|
||||
private static class ContactRequirementException extends FormException {
|
||||
private static class ContactRequirementException extends RuntimeException {
|
||||
ContactRequirementException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.ui.server;
|
||||
@@ -30,7 +30,6 @@ import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.request.Modules;
|
||||
import google.registry.request.auth.AuthModule;
|
||||
import google.registry.ui.ConsoleDebug;
|
||||
import google.registry.util.UtilsModule;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -40,7 +39,6 @@ import jakarta.inject.Singleton;
|
||||
AuthModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
RegistryConfig.ConfigModule.class,
|
||||
ConsoleDebug.ConsoleConfigModule.class,
|
||||
CredentialModule.class,
|
||||
CustomLogicFactoryModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
|
||||
@@ -35,12 +35,6 @@ public final class RegistryTestServer {
|
||||
|
||||
public static final ImmutableMap<String, Path> RUNFILES =
|
||||
new ImmutableMap.Builder<String, Path>()
|
||||
.put(
|
||||
"/index.html",
|
||||
PROJECT_ROOT.resolve("core/src/main/java/google/registry/ui/html/index.html"))
|
||||
.put(
|
||||
"/error.html",
|
||||
PROJECT_ROOT.resolve("core/src/main/java/google/registry/ui/html/error.html"))
|
||||
.put("/console/*", PROJECT_ROOT.resolve("console-webapp/staged/dist"))
|
||||
.build();
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import google.registry.request.auth.AuthResult;
|
||||
import google.registry.request.auth.OidcTokenAuthenticationMechanism;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.tools.params.HostAndPortParameter;
|
||||
import google.registry.ui.ConsoleDebug;
|
||||
import java.util.List;
|
||||
|
||||
/** Command-line interface for {@link RegistryTestServer}. */
|
||||
@@ -42,11 +41,6 @@ public final class RegistryTestServerMain {
|
||||
private static final String LIGHT_PURPLE = "\u001b[38;5;139m";
|
||||
private static final String ORANGE = "\u001b[1;38;5;172m";
|
||||
|
||||
@Parameter(
|
||||
names = "--mode",
|
||||
description = "UI console debug mode. RAW allows live editing; DEBUG allows rename testing.")
|
||||
private ConsoleDebug mode = ConsoleDebug.PRODUCTION;
|
||||
|
||||
@Parameter(
|
||||
names = "--address",
|
||||
description = "Listening address.",
|
||||
@@ -67,14 +61,10 @@ public final class RegistryTestServerMain {
|
||||
arity = 1)
|
||||
private boolean loginIsAdmin = true;
|
||||
|
||||
@Parameter(
|
||||
names = "--jetty_debug",
|
||||
description = "Enables Jetty debug logging.")
|
||||
@Parameter(names = "--jetty_debug", description = "Enables Jetty debug logging.")
|
||||
private boolean jettyDebug;
|
||||
|
||||
@Parameter(
|
||||
names = "--jetty_verbose",
|
||||
description = "Enables Jetty verbose logging.")
|
||||
@Parameter(names = "--jetty_verbose", description = "Enables Jetty verbose logging.")
|
||||
private boolean jettyVerbose;
|
||||
|
||||
@Parameter(
|
||||
@@ -96,7 +86,6 @@ public final class RegistryTestServerMain {
|
||||
}
|
||||
|
||||
private void run() throws Throwable {
|
||||
ConsoleDebug.set(mode);
|
||||
if (jettyDebug) {
|
||||
System.setProperty("DEBUG", "true");
|
||||
}
|
||||
@@ -105,7 +94,7 @@ public final class RegistryTestServerMain {
|
||||
}
|
||||
|
||||
System.out.printf(
|
||||
"""
|
||||
"""
|
||||
|
||||
CHARLESTON ROAD REGISTRY SHARED REGISTRATION SYSTEM
|
||||
ICANN-GTLD-AGB-20120604
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.testing.NullPointerTester;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link FormFieldException}. */
|
||||
class FormFieldExceptionTest {
|
||||
|
||||
@Test
|
||||
void testGetFieldName_multiplePropagations_joinsUsingJsonNotation() {
|
||||
assertThat(
|
||||
new FormFieldException("This field is required.")
|
||||
.propagate("attack")
|
||||
.propagate("cat")
|
||||
.propagate(0)
|
||||
.propagate("lol")
|
||||
.getFieldName())
|
||||
.isEqualTo("lol[0].cat.attack");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFieldName_singlePropagations_noFancyJoining() {
|
||||
assertThat(
|
||||
new FormFieldException("This field is required.")
|
||||
.propagate("cat")
|
||||
.getFieldName())
|
||||
.isEqualTo("cat");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFieldName_noPropagations_throwsIse() {
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> new FormFieldException("This field is required.").getFieldName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullness() {
|
||||
NullPointerTester tester = new NullPointerTester()
|
||||
.setDefault(FormField.class, FormField.named("love").build());
|
||||
tester.testAllPublicConstructors(FormFieldException.class);
|
||||
tester.testAllPublicStaticMethods(FormFieldException.class);
|
||||
tester.testAllPublicInstanceMethods(new FormFieldException("lol"));
|
||||
}
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.collect.Range.atLeast;
|
||||
import static com.google.common.collect.Range.atMost;
|
||||
import static com.google.common.collect.Range.closed;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.testing.NullPointerTester;
|
||||
import com.google.re2j.Pattern;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link FormField}. */
|
||||
class FormFieldTest {
|
||||
|
||||
private enum ICanHazEnum {
|
||||
LOL,
|
||||
CAT
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvert_nullString_notPresent() {
|
||||
assertThat(FormField.named("lol").build().convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvert_emptyString_returnsEmpty() {
|
||||
assertThat(FormField.named("lol").build().convert("")).hasValue("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithDefault_hasValue_returnsValue() {
|
||||
assertThat(FormField.named("lol").withDefault("default").build().convert("return me!"))
|
||||
.hasValue("return me!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithDefault_nullValue_returnsDefault() {
|
||||
assertThat(FormField.named("lol").withDefault("default").build().convert(null))
|
||||
.hasValue("default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyToNull_emptyString_notPresent() {
|
||||
assertThat(FormField.named("lol").emptyToNull().build().convert("")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyToNullRequired_emptyString_throwsFfe() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() -> FormField.named("lol").emptyToNull().required().build().convert(""));
|
||||
assertThat(thrown, equalTo(new FormFieldException("This field is required.").propagate("lol")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyToNull_typeMismatch() {
|
||||
assertThrows(
|
||||
IllegalStateException.class, () -> FormField.named("lol", Object.class).emptyToNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNamedLong() {
|
||||
assertThat(FormField.named("lol", Long.class).build().convert(666L)).hasValue(666L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUppercased() {
|
||||
FormField<String, String> field = FormField.named("lol").uppercased().build();
|
||||
assertThat(field.convert(null)).isEmpty();
|
||||
assertThat(field.convert("foo")).hasValue("FOO");
|
||||
assertThat(field.convert("BAR")).hasValue("BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLowercased() {
|
||||
FormField<String, String> field = FormField.named("lol").lowercased().build();
|
||||
assertThat(field.convert(null)).isEmpty();
|
||||
assertThat(field.convert("foo")).hasValue("foo");
|
||||
assertThat(field.convert("BAR")).hasValue("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIn_passesThroughNull() {
|
||||
FormField<String, String> field =
|
||||
FormField.named("lol").in(ImmutableSet.of("foo", "bar")).build();
|
||||
assertThat(field.convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIn_valueIsContainedInSet() {
|
||||
FormField<String, String> field =
|
||||
FormField.named("lol").in(ImmutableSet.of("foo", "bar")).build();
|
||||
assertThat(field.convert("foo")).hasValue("foo");
|
||||
assertThat(field.convert("bar")).hasValue("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIn_valueMissingFromSet() {
|
||||
FormField<String, String> field =
|
||||
FormField.named("lol").in(ImmutableSet.of("foo", "bar")).build();
|
||||
FormFieldException thrown = assertThrows(FormFieldException.class, () -> field.convert("omfg"));
|
||||
assertThat(thrown, equalTo(new FormFieldException("Unrecognized value.").propagate("lol")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_hasLowerBound_nullValue_passesThrough() {
|
||||
assertThat(FormField.named("lol").range(atLeast(5)).build().convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_minimum_stringLengthEqualToMinimum_doesNothing() {
|
||||
assertThat(FormField.named("lol").range(atLeast(5)).build().convert("hello")).hasValue("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_minimum_stringLengthShorterThanMinimum_throwsFfe() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() -> FormField.named("lol").range(atLeast(4)).build().convert("lol"));
|
||||
assertThat(thrown).hasMessageThat().contains("Number of characters (3) not in range [4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_noLowerBound_nullValue_passThrough() {
|
||||
assertThat(FormField.named("lol").range(atMost(5)).build().convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_maximum_stringLengthEqualToMaximum_doesNothing() {
|
||||
assertThat(FormField.named("lol").range(atMost(5)).build().convert("hello")).hasValue("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_maximum_stringLengthShorterThanMaximum_throwsFfe() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() -> FormField.named("lol").range(atMost(5)).build().convert("omgomg"));
|
||||
assertThat(thrown).hasMessageThat().contains("Number of characters (6) not in range");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_numericTypes() {
|
||||
FormField.named("lol", Byte.class).range(closed(5, 10)).build().convert((byte) 7);
|
||||
FormField.named("lol", Short.class).range(closed(5, 10)).build().convert((short) 7);
|
||||
FormField.named("lol", Integer.class).range(closed(5, 10)).build().convert(7);
|
||||
FormField.named("lol", Long.class).range(closed(5, 10)).build().convert(7L);
|
||||
FormField.named("lol", Float.class).range(closed(5, 10)).build().convert(7F);
|
||||
FormField.named("lol", Double.class).range(closed(5, 10)).build().convert(7D);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRange_typeMismatch() {
|
||||
assertThrows(
|
||||
IllegalStateException.class, () -> FormField.named("lol", Object.class).range(atMost(5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMatches_matches_doesNothing() {
|
||||
assertThat(FormField.named("lol").matches(Pattern.compile("[a-z]+")).build().convert("abc"))
|
||||
.hasValue("abc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMatches_mismatch_throwsFfeAndShowsDefaultErrorMessageWithPattern() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() ->
|
||||
FormField.named("lol")
|
||||
.matches(Pattern.compile("[a-z]+"))
|
||||
.build()
|
||||
.convert("123abc456"));
|
||||
assertThat(
|
||||
thrown, equalTo(new FormFieldException("Must match pattern: [a-z]+").propagate("lol")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMatches_typeMismatch() {
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> FormField.named("lol", Object.class).matches(Pattern.compile(".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetains() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.retains(CharMatcher.anyOf("0123456789"))
|
||||
.build()
|
||||
.convert(" 123 1593-43 453 45 4 4 \t"))
|
||||
.hasValue("1231593434534544");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCast() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.transform(Integer.class, Integer::parseInt)
|
||||
.build()
|
||||
.convert("123"))
|
||||
.hasValue(123);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCast_twice() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.transform(Object.class, Integer::parseInt)
|
||||
.transform(String.class, Object::toString)
|
||||
.build()
|
||||
.convert("123"))
|
||||
.hasValue("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsList_null_notPresent() {
|
||||
assertThat(FormField.named("lol").asList().build().convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsList_empty_returnsEmpty() {
|
||||
assertThat(FormField.named("lol").asList().build().convert(ImmutableList.of()))
|
||||
.hasValue(ImmutableList.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsListEmptyToNullRequired_empty_throwsFfe() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() ->
|
||||
FormField.named("lol")
|
||||
.asList()
|
||||
.emptyToNull()
|
||||
.required()
|
||||
.build()
|
||||
.convert(ImmutableList.of()));
|
||||
assertThat(thrown, equalTo(new FormFieldException("This field is required.").propagate("lol")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListEmptyToNull_empty_notPresent() {
|
||||
assertThat(FormField.named("lol").asList().emptyToNull().build().convert(ImmutableList.of()))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsEnum() {
|
||||
FormField<String, ICanHazEnum> omgField =
|
||||
FormField.named("omg").asEnum(ICanHazEnum.class).build();
|
||||
assertThat(omgField.convert("LOL")).hasValue(ICanHazEnum.LOL);
|
||||
assertThat(omgField.convert("CAT")).hasValue(ICanHazEnum.CAT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsEnum_lowercase_works() {
|
||||
FormField<String, ICanHazEnum> omgField =
|
||||
FormField.named("omg").asEnum(ICanHazEnum.class).build();
|
||||
assertThat(omgField.convert("lol")).hasValue(ICanHazEnum.LOL);
|
||||
assertThat(omgField.convert("cat")).hasValue(ICanHazEnum.CAT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsEnum_badInput_throwsFfe() {
|
||||
FormField<String, ICanHazEnum> omgField =
|
||||
FormField.named("omg").asEnum(ICanHazEnum.class).build();
|
||||
FormFieldException thrown =
|
||||
assertThrows(FormFieldException.class, () -> omgField.convert("helo"));
|
||||
assertThat(
|
||||
thrown,
|
||||
equalTo(
|
||||
new FormFieldException("Enum ICanHazEnum does not contain 'helo'").propagate("omg")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSplitList() {
|
||||
FormField<String, List<String>> field =
|
||||
FormField.named("lol").asList(Splitter.on(',').omitEmptyStrings()).build();
|
||||
assertThat(field.convert("oh,my,goth").get()).containsExactly("oh", "my", "goth").inOrder();
|
||||
assertThat(field.convert("").get()).isEmpty();
|
||||
assertThat(field.convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSplitSet() {
|
||||
FormField<String, Set<String>> field =
|
||||
FormField.named("lol").uppercased().asSet(Splitter.on(',').omitEmptyStrings()).build();
|
||||
assertThat(field.convert("oh,my,goth").get()).containsExactly("OH", "MY", "GOTH").inOrder();
|
||||
assertThat(field.convert("").get()).isEmpty();
|
||||
assertThat(field.convert(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsList() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.asList()
|
||||
.build()
|
||||
.convert(ImmutableList.of("lol", "cat", ""))
|
||||
.get())
|
||||
.containsExactly("lol", "cat", "")
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsList_trimmedEmptyToNullOnItems() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.trimmed()
|
||||
.emptyToNull()
|
||||
.matches(Pattern.compile("[a-z]+"))
|
||||
.asList()
|
||||
.range(closed(1, 2))
|
||||
.build()
|
||||
.convert(ImmutableList.of("lol\n", "\tcat "))
|
||||
.get())
|
||||
.containsExactly("lol", "cat")
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsList_nullElements_getIgnored() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.emptyToNull()
|
||||
.asList()
|
||||
.build()
|
||||
.convert(ImmutableList.of("omg", ""))
|
||||
.get())
|
||||
.containsExactly("omg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsListRequiredElements_nullElement_throwsFfeWithIndex() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() ->
|
||||
FormField.named("lol")
|
||||
.emptyToNull()
|
||||
.required()
|
||||
.asList()
|
||||
.build()
|
||||
.convert(ImmutableList.of("omg", "")));
|
||||
assertThat(
|
||||
thrown,
|
||||
equalTo(new FormFieldException("This field is required.").propagate(1).propagate("lol")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapAsListRequiredElements_nullElement_throwsFfeWithIndexAndKey() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() ->
|
||||
FormField.mapNamed("lol")
|
||||
.transform(
|
||||
String.class,
|
||||
input ->
|
||||
FormField.named("cat")
|
||||
.emptyToNull()
|
||||
.required()
|
||||
.build()
|
||||
.extractUntyped(input)
|
||||
.get())
|
||||
.asList()
|
||||
.build()
|
||||
.convert(ImmutableList.of(ImmutableMap.of("cat", ""))));
|
||||
assertThat(
|
||||
thrown,
|
||||
equalTo(
|
||||
new FormFieldException("This field is required.")
|
||||
.propagate("cat")
|
||||
.propagate(0)
|
||||
.propagate("lol")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsListTrimmed_typeMismatch() {
|
||||
FormField.named("lol").trimmed().asList();
|
||||
assertThrows(IllegalStateException.class, () -> FormField.named("lol").asList().trimmed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsMatrix() {
|
||||
assertThat(
|
||||
FormField.named("lol", Integer.class)
|
||||
.transform(input -> input * 2)
|
||||
.asList()
|
||||
.asList()
|
||||
.build()
|
||||
.convert(
|
||||
Lists.cartesianProduct(
|
||||
ImmutableList.of(ImmutableList.of(1, 2), ImmutableList.of(3, 4))))
|
||||
.get())
|
||||
.containsExactly(
|
||||
ImmutableList.of(2, 6),
|
||||
ImmutableList.of(2, 8),
|
||||
ImmutableList.of(4, 6),
|
||||
ImmutableList.of(4, 8))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsSet() {
|
||||
assertThat(
|
||||
FormField.named("lol")
|
||||
.asSet()
|
||||
.build()
|
||||
.convert(ImmutableList.of("lol", "cat", "cat"))
|
||||
.get())
|
||||
.containsExactly("lol", "cat");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTrimmed() {
|
||||
assertThat(FormField.named("lol").trimmed().build().convert(" \thello \t\n")).hasValue("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTrimmed_typeMismatch() {
|
||||
assertThrows(IllegalStateException.class, () -> FormField.named("lol", Object.class).trimmed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsBuilder() {
|
||||
FormField<String, String> field = FormField.named("omg").uppercased().build();
|
||||
assertThat(field.name()).isEqualTo("omg");
|
||||
assertThat(field.convert("hello")).hasValue("HELLO");
|
||||
field = field.asBuilder().build();
|
||||
assertThat(field.name()).isEqualTo("omg");
|
||||
assertThat(field.convert("hello")).hasValue("HELLO");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsBuilderNamed() {
|
||||
FormField<String, String> field = FormField.named("omg").uppercased().build();
|
||||
assertThat(field.name()).isEqualTo("omg");
|
||||
assertThat(field.convert("hello")).hasValue("HELLO");
|
||||
field = field.asBuilderNamed("bog").build();
|
||||
assertThat(field.name()).isEqualTo("bog");
|
||||
assertThat(field.convert("hello")).hasValue("HELLO");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullness() {
|
||||
NullPointerTester tester =
|
||||
new NullPointerTester()
|
||||
.setDefault(Class.class, Object.class)
|
||||
.setDefault(Function.class, x -> x)
|
||||
.setDefault(Pattern.class, Pattern.compile("."))
|
||||
.setDefault(String.class, "hello.com");
|
||||
tester.testAllPublicStaticMethods(FormField.class);
|
||||
tester.testAllPublicInstanceMethods(FormField.named("lol"));
|
||||
tester.testAllPublicInstanceMethods(FormField.named("lol").build());
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.truth.Truth.assertThat;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.testing.NullPointerTester;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link FormFields}. */
|
||||
class FormFieldsTest {
|
||||
|
||||
@Test
|
||||
void testXsToken_collapsesAndTrimsWhitespace() {
|
||||
assertThat(FormFields.XS_TOKEN.convert(" hello \r\n\t there\n")).hasValue("hello there");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsNormalizedString_extraSpaces_doesntCare() {
|
||||
assertThat(FormFields.XS_NORMALIZED_STRING.convert("hello there")).hasValue("hello there");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsNormalizedString_sideSpaces_doesntCare() {
|
||||
assertThat(FormFields.XS_NORMALIZED_STRING.convert(" hello there ")).hasValue(" hello there ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsNormalizedString_containsNonSpaceWhitespace_fails() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class,
|
||||
() -> FormFields.XS_NORMALIZED_STRING.convert(" hello \r\n\t there\n"));
|
||||
assertThat(
|
||||
thrown,
|
||||
equalTo(
|
||||
new FormFieldException("Must not contain tabs or multiple lines.")
|
||||
.propagate("xsNormalizedString")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppE164PhoneNumber_nanpaNumber_validates() {
|
||||
assertThat(FormFields.XS_NORMALIZED_STRING.convert("+1.2125650000")).hasValue("+1.2125650000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppE164PhoneNumber_londonNumber_validates() {
|
||||
assertThat(FormFields.XS_NORMALIZED_STRING.convert("+44.2011112222"))
|
||||
.hasValue("+44.2011112222");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppE164PhoneNumber_localizedNumber_fails() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(
|
||||
FormFieldException.class, () -> FormFields.PHONE_NUMBER.convert("(212) 565-0000"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Must be a valid +E.164 phone number, e.g. +1.2125650000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppE164PhoneNumber_appliesXsTokenTransform() {
|
||||
assertThat(FormFields.PHONE_NUMBER.convert(" +1.2125650000 \r")).hasValue("+1.2125650000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppRoid_correctSyntax_validates() {
|
||||
assertThat(FormFields.ROID.convert("SH8013-REP")).hasValue("SH8013-REP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppRoid_lowerCase_validates() {
|
||||
assertThat(FormFields.ROID.convert("sh8013-rep")).hasValue("sh8013-rep");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppRoid_missingHyphen_fails() {
|
||||
FormFieldException thrown =
|
||||
assertThrows(FormFieldException.class, () -> FormFields.ROID.convert("SH8013REP"));
|
||||
assertThat(thrown).hasMessageThat().contains("Please enter a valid EPP ROID, e.g. SH8013-REP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testXsEppRoid_appliesXsTokenTransform() {
|
||||
assertThat(FormFields.ROID.convert("\n FOO-BAR \r")).hasValue("FOO-BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullness() {
|
||||
new NullPointerTester().testAllPublicStaticMethods(FormFields.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user