com.github.mvysny.karibudsl.v10.BinderUtils.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of karibu-dsl Show documentation
Show all versions of karibu-dsl Show documentation
Karibu-DSL, Kotlin extensions/DSL for Vaadin
The newest version!
package com.github.mvysny.karibudsl.v10
import com.github.mvysny.kaributools.BrowserTimeZone
import com.vaadin.flow.data.binder.*
import com.vaadin.flow.data.converter.*
import com.vaadin.flow.component.HasValue
import com.vaadin.flow.component.textfield.EmailField
import com.vaadin.flow.component.textfield.TextArea
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.data.validator.*
import java.lang.reflect.Method
import java.math.BigDecimal
import java.math.BigInteger
import java.time.*
import java.util.*
import kotlin.reflect.KMutableProperty1
import java.time.LocalDateTime
/**
* Trims the user input string before storing it into the underlying property data source. Vital for mobile-oriented apps:
* Android keyboard often adds whitespace to the end of the text when auto-completion occurs. Imagine storing a username ending with a space upon registration:
* such person can no longer log in from his PC unless he explicitly types in the space.
* @param blanksToNulls if true then a blank String value is passed as `null` to the model. Defaults to false.
*/
public fun Binder.BindingBuilder.trimmingConverter(blanksToNulls: Boolean = false): Binder.BindingBuilder =
withConverter(object : Converter {
override fun convertToModel(value: String?, context: ValueContext?): Result {
var trimmedValue: String? = value?.trim()
if (blanksToNulls && trimmedValue.isNullOrBlank()) {
trimmedValue = null
}
return Result.ok(trimmedValue)
}
override fun convertToPresentation(value: String?, context: ValueContext?): String {
// must not return null here otherwise TextField will fail with NPE:
// workaround for https://github.com/vaadin/framework/issues/8664
return value ?: ""
}
})
/**
* Converts [String] from [textField] to [Int]-typed bean field.
*
* It's probably better to use [integerField] directly instead.
*/
public fun Binder.BindingBuilder.toInt(): Binder.BindingBuilder =
withConverter(StringToIntegerConverter(karibuDslI18n("cantConvertToInteger")))
/**
* Converts [Double] from [numberField] to [Int]-typed bean field.
*
* It's probably better to use [integerField] directly instead.
*/
@JvmName("doubleToInt")
public fun Binder.BindingBuilder.toInt(): Binder.BindingBuilder =
withConverter(DoubleToIntConverter)
/**
* Converts [String] from [textField] to [Double]-typed bean field.
*/
public fun Binder.BindingBuilder.toDouble(
errorMessage: String = karibuDslI18n("cantConvertToDecimal")
): Binder.BindingBuilder =
withConverter(StringToDoubleConverter(errorMessage))
/**
* Converts [String] from [textField] to [Long]-typed bean field.
*/
public fun Binder.BindingBuilder.toLong(
errorMessage: String = karibuDslI18n("cantConvertToInteger")
): Binder.BindingBuilder =
withConverter(StringToLongConverter(errorMessage))
/**
* Converts [Double] from [numberField] to [Long]-typed bean field.
*
* It's probably better to use [integerField] and int-to-long conversion instead.
*/
@JvmName("doubleToLong")
public fun Binder.BindingBuilder.toLong(): Binder.BindingBuilder =
withConverter(DoubleToLongConverter)
/**
* Converts [Int] from [integerField] to [Long]-typed bean field.
*/
@JvmName("intToLong")
public fun Binder.BindingBuilder.toLong(): Binder.BindingBuilder =
withConverter(IntToLongConverter)
/**
* Converts [String] from [textField] to [BigDecimal]-typed bean field.
*
* It's probably better to use [bigDecimalField] directly instead.
*/
public fun Binder.BindingBuilder.toBigDecimal(
errorMessage: String = karibuDslI18n("cantConvertToDecimal")
): Binder.BindingBuilder =
withConverter(StringToBigDecimalConverter(errorMessage))
/**
* Converts [Double] from [numberField] to [BigDecimal]-typed bean field.
*
* It's probably better to use [bigDecimalField] directly instead.
*/
@JvmName("doubleToBigDecimal")
public fun Binder.BindingBuilder.toBigDecimal(): Binder.BindingBuilder =
withConverter(DoubleToBigDecimalConverter)
/**
* Converts [String] from [textField] to [BigInteger]-typed bean field.
*/
public fun Binder.BindingBuilder.toBigInteger(): Binder.BindingBuilder =
withConverter(StringToBigIntegerConverter(karibuDslI18n("cantConvertToInteger")))
/**
* Converts [Double] from [numberField] to [BigInteger]-typed bean field.
*/
@JvmName("doubleToBigInteger")
public fun Binder.BindingBuilder.toBigInteger(): Binder.BindingBuilder =
withConverter(DoubleToBigIntegerConverter)
/**
* Converts [LocalDate] from [datePicker] to [Date]-typed bean field. Uses [browserTimeZone].
*/
public fun Binder.BindingBuilder.toDate(): Binder.BindingBuilder =
withConverter(LocalDateToDateConverter(BrowserTimeZone.get))
/**
* Converts [LocalDateTime] from [dateTimePicker] to [Date]-typed bean field. Uses [browserTimeZone].
*/
@JvmName("localDateTimeToDate")
public fun Binder.BindingBuilder.toDate(): Binder.BindingBuilder =
withConverter(LocalDateTimeToDateConverter(BrowserTimeZone.get))
/**
* Converts [LocalDate] from [datePicker] to [Instant]-typed bean field. Uses [browserTimeZone].
*/
public fun Binder.BindingBuilder.toInstant(): Binder.BindingBuilder =
withConverter(LocalDateToInstantConverter(BrowserTimeZone.get))
/**
* Converts [LocalDateTime] from [dateTimePicker] to [Instant]-typed bean field. Uses [browserTimeZone].
*/
@JvmName("localDateTimeToInstant")
public fun Binder.BindingBuilder.toInstant(): Binder.BindingBuilder =
withConverter(LocalDateTimeToInstantConverter(BrowserTimeZone.get))
/**
* Converts [LocalDate] from [datePicker] to [Calendar]-typed bean field. Uses [browserTimeZone].
*/
public fun Binder.BindingBuilder.toCalendar(): Binder.BindingBuilder =
toDate().withConverter(DateToCalendarConverter)
/**
* Converts [LocalDateTime] from [dateTimePicker] to [Calendar]-typed bean field. Uses [browserTimeZone].
*/
@JvmName("localDateTimeToCalendar")
public fun Binder.BindingBuilder.toCalendar(): Binder.BindingBuilder =
toDate().withConverter(DateToCalendarConverter)
/**
* Allows you to create [BeanValidationBinder] like this: `beanValidationBinder()` instead of `BeanValidationBinder(Person::class.java)`
*/
public inline fun beanValidationBinder(): BeanValidationBinder = BeanValidationBinder(T::class.java)
/**
* Allows you to bind the component directly in the component's definition. E.g.
* ```
* textField("Name:") {
* bind(binder).bind(Person::name)
* }
* ```
* @param nullToBlank if true (the default), null bean field value is converted to
* an empty String automatically, to prevent NPEs in [TextField]. However, the problem is
* that an empty string is converted to null value on its way back, which causes an exception
* if the target Kotlin field is non-nullable. Set to false if you're binding to a non-nullable Kotlin field.
*/
@JvmOverloads
public fun HasValue<*, FIELDVALUE>.bind(
binder: Binder,
nullToBlank: Boolean = true
): Binder.BindingBuilder {
var builder: Binder.BindingBuilder = binder.forField(this)
// fix NPE for TextField and TextArea by having a converter which converts null to "" and back.
if (nullToBlank) {
if (this is TextField || this is TextArea || this is EmailField) {
@Suppress("UNCHECKED_CAST")
builder = builder.withNullRepresentation("" as FIELDVALUE)
}
}
return builder
}
/**
* A type-safe binding which binds only to a property of given type, found on given bean.
* @param prop the bean property
*/
public fun Binder.BindingBuilder.bind(prop: KMutableProperty1): Binder.Binding {
// oh crap, don't use binding by getter and setter - validations won't work!
// we need to use bind(String) even though that will use undebuggable crappy Java 8 lambdas :-(
// bind({ bean -> prop.get(bean) }, { bean, value -> prop.set(bean, value) })
var name = prop.name
if (name.startsWith("is")) {
// Kotlin KProperties named "isFoo" are represented with just "foo" in the bean property set
name = name[2].lowercase() + name.drop(3)
}
return bind(name)
}
/**
* A converter that converts from [LocalDate] [datePicker] to [Instant] bean field.
* @property zoneId the time zone id to use.
*/
public class LocalDateToInstantConverter(public val zoneId: ZoneId = BrowserTimeZone.get) : Converter {
override fun convertToModel(localDate: LocalDate?, context: ValueContext): Result =
Result.ok(localDate?.atStartOfDay(zoneId)?.toInstant())
override fun convertToPresentation(date: Instant?, context: ValueContext): LocalDate? =
date?.atZone(zoneId)?.toLocalDate()
}
/**
* A converter that converts from [LocalDateTime] [dateTimePicker] to [Instant] bean field.
* @property zoneId the time zone to use
*/
public class LocalDateTimeToInstantConverter(public val zoneId: ZoneId = BrowserTimeZone.get) : Converter {
override fun convertToModel(localDate: LocalDateTime?, context: ValueContext): Result =
Result.ok(localDate?.atZone(zoneId)?.toInstant())
override fun convertToPresentation(date: Instant?, context: ValueContext): LocalDateTime? =
date?.atZone(zoneId)?.toLocalDateTime()
}
/**
* Converts [Double] from [numberField] to [Int]-typed bean field.
*
* It's probably better to use [integerField] directly instead.
*/
public object DoubleToIntConverter : Converter {
override fun convertToPresentation(value: Int?, context: ValueContext?): Double? = value?.toDouble()
override fun convertToModel(value: Double?, context: ValueContext?): Result = Result.ok(value?.toInt())
}
/**
* Converts [Double] from [numberField] to [Long]-typed bean field.
*
* It's probably better to use [integerField] and int-to-long conversion instead.
*/
public object DoubleToLongConverter : Converter {
override fun convertToPresentation(value: Long?, context: ValueContext?): Double? = value?.toDouble()
override fun convertToModel(value: Double?, context: ValueContext?): Result = Result.ok(value?.toLong())
}
/**
* Converts [Int] from [integerField] to [Long]-typed bean field.
*/
public object IntToLongConverter : Converter {
override fun convertToPresentation(value: Long?, context: ValueContext?): Int? = value?.toInt()
override fun convertToModel(value: Int?, context: ValueContext?): Result = Result.ok(value?.toLong())
}
/**
* Converts [Double] from [numberField] to [BigDecimal]-typed bean field.
*
* It's probably better to use [bigDecimalField] directly instead.
*/
public object DoubleToBigDecimalConverter : Converter {
override fun convertToPresentation(value: BigDecimal?, context: ValueContext?): Double? = value?.toDouble()
override fun convertToModel(value: Double?, context: ValueContext?): Result = Result.ok(value?.toBigDecimal())
}
/**
* Converts [Double] from [numberField] to [BigInteger]-typed bean field.
*/
public object DoubleToBigIntegerConverter : Converter {
override fun convertToPresentation(value: BigInteger?, context: ValueContext?): Double? = value?.toDouble()
override fun convertToModel(value: Double?, context: ValueContext?): Result {
val bi = if (value == null) null else BigInteger(value.toLong().toString())
return Result.ok(bi)
}
}
/**
* Converts [Date] to [Calendar]-typed bean field. Append to [LocalDateTimeToDateConverter] or
* [LocalDateToDateConverter] to allow for LocalDate-to-Calendar conversions.
*/
public object DateToCalendarConverter : Converter {
override fun convertToModel(value: Date?, context: ValueContext?): Result = when (value) {
null -> Result.ok(null)
else -> {
val calendar = Calendar.getInstance()
calendar.time = value
Result.ok(calendar)
}
}
override fun convertToPresentation(value: Calendar?, context: ValueContext?): Date? = value?.time
}
public class StringNotBlankValidator(public val errorMessage: String = "must not be blank") : Validator {
override fun apply(value: String?, context: ValueContext): ValidationResult = when {
value.isNullOrBlank() -> ValidationResult.error(errorMessage)
else -> ValidationResult.ok()
}
}
public fun Binder.BindingBuilder.validateNotBlank(
errorMessage: String = "must not be blank"
): Binder.BindingBuilder =
withValidator(StringNotBlankValidator(errorMessage))
public fun Binder.BindingBuilder.validEmail(
errorMessage: String = "must be a valid email address"
): Binder.BindingBuilder =
withValidator(EmailValidator(errorMessage))
public fun Binder.BindingBuilder.validateInRange(range: ClosedRange): Binder.BindingBuilder =
withValidator(FloatRangeValidator("must be in $range", range.start, range.endInclusive))
@JvmName("validateIntInRange")
public fun Binder.BindingBuilder.validateInRange(range: IntRange): Binder.BindingBuilder =
withValidator(IntegerRangeValidator("must be in $range", range.start, range.endInclusive))
@JvmName("validateLongInRange")
public fun Binder.BindingBuilder.validateInRange(range: LongRange): Binder.BindingBuilder =
withValidator(LongRangeValidator("must be in $range", range.start, range.endInclusive))
@JvmName("validateDoubleInRange")
public fun Binder.BindingBuilder.validateInRange(range: ClosedRange): Binder.BindingBuilder =
withValidator(DoubleRangeValidator("must be in $range", range.start, range.endInclusive))
@JvmName("validateBigIntegerInRange")
public fun Binder.BindingBuilder.validateInRange(range: ClosedRange): Binder.BindingBuilder =
withValidator(BigIntegerRangeValidator("must be in $range", range.start, range.endInclusive))
@JvmName("validateBigDecimalInRange")
public fun Binder.BindingBuilder.validateInRange(range: ClosedRange): Binder.BindingBuilder =
withValidator(BigDecimalRangeValidator("must be in $range", range.start, range.endInclusive))
private val _Binder_getBindings: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m: Method = Binder::class.java.getDeclaredMethod("getBindings")
m.isAccessible = true
m
}
@Suppress("UNCHECKED_CAST", "ConflictingExtensionProperty")
private val Binder.bindings: Collection>
get() = _Binder_getBindings.invoke(this) as Collection>
/**
* Guesses whether the binder has been configured with read-only.
*
* Since Binder doesn't remember whether it is read-only, we have to guess.
*/
@Suppress("UNCHECKED_CAST")
public val Binder<*>.guessIsReadOnly: Boolean
get() = bindings.any { it.setter != null && it.isReadOnly }
/**
* Workaround for [Flow #17277](https://github.com/vaadin/flow/issues/17277) and [#13](https://github.com/mvysny/karibu-dsl/issues/13).
* Always use this instead of [Binder.BindingBuilder.asRequired] until the ticket above is resolved.
*/
public fun Binder.BindingBuilder.asRequiredNotNull(errorMessage: String = ""): Binder.BindingBuilder {
val validator = Validator { value, _ ->
when {
value == null || value == field.emptyValue -> ValidationResult.error(errorMessage)
else -> ValidationResult.ok()
}
}
return asRequired(validator)
}