/** * Jooby https://jooby.io * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ package io.jooby; import io.jooby.exception.MissingValueException; import io.jooby.exception.TypeMismatchException; import io.jooby.internal.ArrayValue; import io.jooby.internal.HashValue; import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import java.util.TreeMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; /** * Unified API for HTTP value. This API plays two role: * * - unify access to HTTP values, like query, path, form and header parameter * - works as validation API, because it is able to check for required and type-safe values * * The value API is composed by three types: * * - Single value * - Object value * - Sequence of values (array) * * Single value are can be converted to string, int, boolean, enum like values. * Object value is a key:value structure (like a hash). * Sequence of values are index based structure. * * All these 3 types are modeled into a single Value class. At any time you can treat a value as * 1) single, 2) hash or 3) array of them. * * @since 2.0.0 * @author edgar */ public interface Value { /** * Convert this value to long (if possible). * * @return Long value. */ default long longValue() { try { return Long.parseLong(value()); } catch (NumberFormatException x) { try { LocalDateTime date = LocalDateTime.parse(value(), Context.RFC1123); Instant instant = date.toInstant(ZoneOffset.UTC); return instant.toEpochMilli(); } catch (DateTimeParseException expected) { } throw new TypeMismatchException(name(), long.class, x); } } /** * Convert this value to long (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to long (if possible) or fallback to given value when missing. */ default long longValue(long defaultValue) { try { return longValue(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to int (if possible). * * @return Int value. */ default int intValue() { try { return Integer.parseInt(value()); } catch (NumberFormatException x) { throw new TypeMismatchException(name(), int.class, x); } } /** * Convert this value to int (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to int (if possible) or fallback to given value when missing. */ default int intValue(int defaultValue) { try { return intValue(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to byte (if possible). * * @return Convert this value to byte (if possible). */ default byte byteValue() { try { return Byte.parseByte(value()); } catch (NumberFormatException x) { throw new TypeMismatchException(name(), byte.class, x); } } /** * Convert this value to byte (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to byte (if possible) or fallback to given value when missing. */ default byte byteValue(byte defaultValue) { try { return byteValue(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to float (if possible). * * @return Convert this value to float (if possible). */ default float floatValue() { try { return Float.parseFloat(value()); } catch (NumberFormatException x) { throw new TypeMismatchException(name(), float.class, x); } } /** * Convert this value to float (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to float (if possible) or fallback to given value when missing. */ default float floatValue(float defaultValue) { try { return floatValue(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to double (if possible). * * @return Convert this value to double (if possible). */ default double doubleValue() { try { return Double.parseDouble(value()); } catch (NumberFormatException x) { throw new TypeMismatchException(name(), double.class, x); } } /** * Convert this value to double (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to double (if possible) or fallback to given value when missing. */ default double doubleValue(double defaultValue) { try { return doubleValue(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to boolean (if possible). * * @return Convert this value to boolean (if possible). */ default boolean booleanValue() { return Boolean.parseBoolean(value()); } /** * Convert this value to boolean (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to boolean (if possible) or fallback to given value when missing. */ default boolean booleanValue(boolean defaultValue) { try { return booleanValue(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to String (if possible) or fallback to given value when missing. * * @param defaultValue Default value. * @return Convert this value to String (if possible) or fallback to given value when missing. */ @Nonnull default String value(@Nonnull String defaultValue) { try { return value(); } catch (MissingValueException x) { return defaultValue; } } /** * Convert this value to String (if possible) or null when missing. * * @return Convert this value to String (if possible) or null when missing. */ @Nullable default String valueOrNull() { return value((String) null); } /** * Convert value using the given function. * * @param fn Function. * @param Target type. * @return Converted value. */ @Nonnull default T value(@Nonnull SneakyThrows.Function fn) { return fn.apply(value()); } /** * Get string value. * * @return String value. */ @Nonnull String value(); /** * Get list of values. * * @return List of values. */ @Nonnull List toList(); /** * Get set of values. * * @return set of values. */ @Nonnull Set toSet(); /** * Convert this value to an Enum. * * @param fn Mapping function. * @param Enum type. * @return Enum. */ @Nonnull default > T toEnum(@Nonnull SneakyThrows.Function fn) { return toEnum(fn, String::toUpperCase); } /** * Convert this value to an Enum. * * @param fn Mapping function. * @param nameProvider Enum name provider. * @param Enum type. * @return Enum. */ @Nonnull default > T toEnum(@Nonnull SneakyThrows.Function fn, @Nonnull Function nameProvider) { return fn.apply(nameProvider.apply(value())); } /** * Get a value or empty optional. * * @return Value or empty optional. */ @Nonnull default Optional toOptional() { try { return Optional.of(value()); } catch (MissingValueException x) { return Optional.empty(); } } /** * True if this is a single value (not a hash or array). * * @return True if this is a single value (not a hash or array). */ default boolean isSingle() { return this instanceof SingleValue; } /** * True for missing values. * * @return True for missing values. */ default boolean isMissing() { return this instanceof MissingValue; } /** * True for present values. * * @return True for present values. */ default boolean isPresent() { return !isMissing(); } /** * True if this value is an array/sequence (not single or hash). * * @return True if this value is an array/sequence. */ default boolean isArray() { return this instanceof ArrayValue; } /** * True if this is a hash/object value (not single or array). * * @return True if this is a hash/object value (not single or array). */ default boolean isObject() { return this instanceof HashValue; } /** * Name of this value or null. * * @return Name of this value or null. */ @Nullable String name(); /** * Get a value or empty optional. * * @param type Item type. * @param Item type. * @return Value or empty optional. */ @Nonnull default Optional toOptional(@Nonnull Class type) { try { return Optional.ofNullable(to(type)); } catch (MissingValueException x) { return Optional.empty(); } } /** * Get list of the given type. * * @param type Type to convert. * @param Item type. * @return List of items. */ @Nonnull default List toList(@Nonnull Class type) { return Collections.singletonList(to(type)); } /** * Get set of the given type. * * @param type Type to convert. * @param Item type. * @return Set of items. */ @Nonnull default Set toSet(@Nonnull Class type) { return Collections.singleton(to(type)); } /** * Convert this value to the given type. Support values are single-value, array-value and * object-value. Object-value can be converted to a JavaBean type. * * @param type Type to convert. * @param Element type. * @return Instance of the type. */ @Nonnull T to(@Nonnull Class type); /** * Value as multi-value map. * * @return Value as multi-value map. */ @Nullable Map> toMultimap(); /** * Value as single-value map. * * @return Value as single-value map. */ default @Nonnull Map toMap() { Map map = new LinkedHashMap<>(); toMultimap().forEach((k, v) -> map.put(k, v.get(0))); return map; } /* *********************************************************************************************** * Factory methods * *********************************************************************************************** */ /** * Creates a missing value. * * @param name Name of missing value. * @return Missing value. */ static @Nonnull ValueNode missing(@Nonnull String name) { return new MissingValue(name); } /** * Creates a single value. * * @param ctx Current context. * @param name Name of value. * @param value Value. * @return Single value. */ static @Nonnull ValueNode value(@Nonnull Context ctx, @Nonnull String name, @Nonnull String value) { return new SingleValue(ctx, name, value); } /** * Creates a sequence/array of values. * * @param ctx Current context. * @param name Name of array. * @param values Field values. * @return Array value. */ static @Nonnull ValueNode array(@Nonnull Context ctx, @Nonnull String name, @Nonnull List values) { return new ArrayValue(ctx, name) .add(values); } /** * Creates a value that fits better with the given values. * * - For null/empty values. It produces a missing value. * - For single element (size==1). It produces a single value * - For multi-value elements (size>1). It produces an array value. * * @param ctx Current context. * @param name Field name. * @param values Field values. * @return A value. */ static @Nonnull ValueNode create(Context ctx, @Nonnull String name, @Nullable List values) { if (values == null || values.size() == 0) { return missing(name); } if (values.size() == 1) { return value(ctx, name, values.get(0)); } return new ArrayValue(ctx, name) .add(values); } /** * Creates a value that fits better with the given values. * * - For null/empty values. It produces a missing value. * - For single element (size==1). It produces a single value * * @param ctx Current context. * @param name Field name. * @param value Field values. * @return A value. */ static @Nonnull ValueNode create(Context ctx, @Nonnull String name, @Nullable String value) { if (value == null) { return missing(name); } return value(ctx, name, value); } /** * Create a hash/object value using the map values. * * @param ctx Current context. * @param values Map values. * @return A hash/object value. */ static @Nonnull ValueNode hash(Context ctx, @Nonnull Map> values) { HashValue node = new HashValue(ctx, null); node.put(values); return node; } /** * Create a hash/object value using the map values. * * @param ctx Current context. * @param values Map values. * @return A hash/object value. */ static @Nonnull ValueNode headers(Context ctx, @Nonnull Map> values) { HashValue node = new HashValue(ctx, null, () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)); node.put(values); return node; } }