Kotlin validator for List<Pair<A, B>> doesn't work - kotlin

I have a data class which I need to validate:
import javax.validation.Valid
import whatever.pckg.validation.PkiSignWithBusinessCode
import whatever.pckg.validation.NullOrNotBlank
data class UploadFileReq(
val id: String? = null,
...(other fields)...
#get:Valid
val signaturesInfo: MutableList<Pair<SignatureInfo, Object>> = mutableListOf() # Object here is for simplicity
) {
#PkiSignWithBusinessCode
data class SignatureInfo(
val typeSign: String = "",
#get:NullOrNotBlank
val businessCode: String? = null,
)
}
#NullOrNotBlank annotation is just a simple merge of standard #NotBlank and #Null annotations.
I also have another custom validation annotation #PkiSignWithBusinessCode, its definition is below:
import whatever.pckg.UploadFileReq
import javax.validation.*
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.reflect.KClass
#Constraint(validatedBy = [PkiSignWithBusinessCodeValidator::class])
#Target(AnnotationTarget.CLASS)
#Retention(RUNTIME)
annotation class PkiSignWithBusinessCode(
val message: String = "PKI signature requires filled businessCode",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
class PkiSignWithBusinessCodeValidator: ConstraintValidator<PkiSignWithBusinessCode, UploadFileReq.SignatureInfo>> {
override fun isValid(obj: UploadFileReq.SignatureInfo?, context: ConstraintValidatorContext): Boolean {
if (obj != null) {
if ((obj.typeSign == "PKI") && (obj.businessCode == null)) {
return false
}
}
return true
}
Logic of above annotation is quite simple - when typeSign equals PKI and businessCode is null, then validator should treat that as invalid object.
For your reference here's a simple unit-test that tries to check the work of #PkiSignWithBusinessCode:
import org.junit.jupiter.api.Test
import whatever.pckg.UploadFileReq
import javax.validation.Validation
import kotlin.test.assertEquals
class PkiSignWithBusinessCodeTest {
#Test
fun `validate PkiSignWithBusinessCodeTest`() {
val validator = Validation.buildDefaultValidatorFactory().validator
val signatureInfo = UploadFileReq.SignatureInfo(
typeSign = "PKI",
businessCode = null
)
val uploadFileReq = UploadFileReq(
null,
signaturesInfo = mutableListOf(signatureInfo to Object)
)
val result = validator.validate(uploadFileReq)
assertEquals(1, result.size)
assertEquals("PKI signature requires filled businessCode", result.first().messageTemplate)
}
}
But this test obviously fails on first assertion state: java.lang.AssertionError: Expected <1>, actual <0>. So no constraint violations found by validator.
The problem is that Spring ignores validation rule of above annotation. As an assumption I suppose that somehow Pair class wrap prevents Spring from using my validation annotation. Maybe it's a bug?
Or maybe I overlooked something in my code?

Found a workaround on this - need to make own ValidatingPair with #Valid annotations on first and second members of this new Pair:
import javax.validation.Valid
data class ValidatingPair<out A, out B>(
#get:Valid
public val first: A,
#get:Valid
public val second: B
) : java.io.Serializable {
override fun toString(): String = "($first, $second)"
}
And make:
val signaturesInfo: MutableList<Pair<SignatureInfo, Object>>
to become
val signaturesInfo: MutableList<ValidatingPair<SignatureInfo, Object>>
Then validation starts working for list members.

Related

Add property path to custom validation in Micronaut

Given the below, working validator factory, is it possible somehow to manipulate propertyPath? When debugging, I can see propertyPath inside io.micronaut.validation.validator.DefaultValidator$DefaultConstraintValidatorContext.
Motivation for doing so, is that some fields are conditionally required, and I need to put validation annotation on class-level, but then propertyPath is set to class, not to the actual fields with validation errors. When returning validation errors to API-clients, it would be nice to have correct property name for validation errors.
import io.micronaut.context.annotation.Factory
import io.micronaut.validation.validator.constraints.ConstraintValidator
import jakarta.inject.Singleton
import no.mycompany.myapp.web.config.UiConfig
import no.mycompany.myapp.web.viewmodel.MyExampleFormVm
#Factory
class ExampleValidatorFactory(private val uiConfig: UiConfig) {
#Singleton
fun validForm(): ConstraintValidator<ValidForm, MyExampleFormVm> = ConstraintValidator { value, _, context ->
val formTypeFromConfig = uiConfig.formTypes.first { it.id == value.formType }
if (formTypeFromConfig.labelSubCompanyId != null && value.subCompanyId == null) {
context.messageTemplate("Form requires one or more sub-company-ids")
// TODO: set propertyPath to "subCompanyId"
return#ConstraintValidator false
}
if (formTypeFromConfig.labelCompanyId != null && value.companyId == null) {
context.messageTemplate("Form requires company-id")
// TODO: set propertyPath to "companyId"
return#ConstraintValidator false
}
true
}
}
Annotation
import javax.validation.Constraint
#Retention(AnnotationRetention.RUNTIME)
#Constraint(validatedBy = [])
annotation class ValidForm(
val message: String = "Invalid form ({validatedValue})"
)
Example bean to validate:
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Pattern
#Introspected
#ValidForm
data class MyExampleFormVm(
#field:NotBlank
val formType: String = "",
#field:Pattern(regexp = "[8|9]\\d{8}")
val companyId: String? = null,
#field:Pattern(regexp = "[8|9]\\d{8}")
val subCompanyId: String? = null
)
This is my custom ConstraintExceptionHandler. I want it.propertyPath.lastOrNull()?.name to return e.g. "subCompanyId".
import io.micronaut.context.annotation.Replaces
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Produces
import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor
import io.micronaut.validation.exceptions.ConstraintExceptionHandler
import jakarta.inject.Singleton
import java.time.Instant
import java.util.*
import javax.validation.ConstraintViolationException
#Produces
#Singleton
#Replaces(ConstraintExceptionHandler::class)
#Requires(classes = [ConstraintViolationException::class, ConstraintExceptionHandler::class])
class CustomConstraintExceptionHandler(responseProcessor: ErrorResponseProcessor<*>) :
ConstraintExceptionHandler(responseProcessor) {
override fun handle(request: HttpRequest<*>, exception: ConstraintViolationException): HttpResponse<*> =
HttpResponse.badRequest(
ApiError(
timestamp = Instant.now().toEpochMilli(),
httpStatusCode = HttpStatus.BAD_REQUEST.code,
errorType = ApiErrorType.VALIDATION_ERROR,
url = request.path,
validationErrors = exception.constraintViolations
.associate { (it.propertyPath.lastOrNull()?.name ?: FALLBACK_PROPERTY_PATH) to it.message }
))
companion object {
const val FALLBACK_PROPERTY_PATH = "ROOT"
}
}
---
import com.fasterxml.jackson.annotation.JsonInclude
import io.micronaut.core.annotation.Introspected
#Introspected
#JsonInclude(value = JsonInclude.Include.NON_NULL)
data class ApiError(
val timestamp: Long,
val httpStatusCode: Int,
val errorType: ApiErrorType,
val url: String,
val validationErrors: Map<String, String>? = null
)
enum class ApiErrorType {
VALIDATION_ERROR,
SYSTEM_ERROR;
}
Question from #Bonatti
If it.propertyPath.lastOrNull() is null, on the Handler, then that is
weird.
If you do like this, resultMap will contain an entry with key="ROOT".
val exampleForm = MyExampleFormVm(
formType = "SomeValue",
companyId = "912345678",
subCompanyId = null
)
val constraintViolations: Set<ConstraintViolation<MyExampleFormVm>> = validator.validate(exampleForm)
val resultMap = constraintViolations.associate { (it.propertyPath.lastOrNull()?.name ?: "ROOT") to it.message }
Environment: Micronaut 3.7.4

Type initialization with jackson?

I'm working on data class extension with polymorphic property. Here's the dataclass:
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
data class CarModelResponse(
val models: List<CarType> = listOf(),
)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(MercedesType::class, name = "mercedes"),
JsonSubTypes.Type(OpelType::class, name = "opel"),
)
abstract class CarType(open val type: String) {
abstract fun getCarFeature(): Any
}
data class MercedesType(
val comfortClass: Int
) : CarType("mercedes") {
override fun getCarFeature(): Int = comfortClass
}
data class OpelType(
val coupon: String
) : CarType("opel") {
override fun getCarFeature(): String = coupon
}
and also i have a test, where i'm trying to get a typed car variable:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Test
class CarResponseTest {
#Test
fun getCarFeatures() {
val json: String = """
{
"models": [
{
"type": "mercedes",
"comfortClass": 1
},
{
"type": "opel",
"coupon": "Buy Opel and win a Mercedes! Coupon #1."
}
]
}""".trimIndent()
val response = jacksonObjectMapper().readValue<CarModelResponse>(json)
val comfortClass: Int = (response.models.first() as MercedesType).getCarFeature()
val couppon: String = (response.models.last() as OpelType).getCarFeature()
}
}
Deserialization works fine, but I need to retrieve car's feature without casting with as at last two lines of code, but I'm a little stuck how to do that. Could anyone advice how can I get rid of it?
You can't do it with no casting, because compiler needs to know what type must be assigned, however in this case you can avoid casting to MercedesType or OpelType:
val comfortClass: Int = response.models.first().getCarFeature() as Int
val coupon: String = response.models.last().getCarFeature() as String

Why is Kotlin's generateSequence returning one too many items in the example below?

I'm calculating the projection of instants in time based on a cron expression and returning them as a Sequence. Here's the class:
// (package omitted)
import org.springframework.scheduling.support.CronExpression
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
class Recurrence(val cronExpression: String) {
private val cron = CronExpression.parse(cronExpression)
fun instants(
fromInclusive: LocalDate = LocalDate.now(),
toExclusive: LocalDate = fromInclusive.plusMonths(1)
): Sequence<LocalDateTime> = instants(fromInclusive.atStartOfDay(), toExclusive.atStartOfDay())
fun instants(
fromInclusive: LocalDateTime = LocalDateTime.now(),
toExclusive: LocalDateTime = fromInclusive.plusMonths(1)
): Sequence<LocalDateTime> {
return generateSequence(cron.next(fromInclusive.minusNanos(1))) {
if (it.isBefore(toExclusive)) {
cron.next(it)
} else {
null
}
}
}
}
The following test fails because the first assertion is false: the returned list has one extra, unexpected element at the end.
// (package omitted)
import java.time.LocalDate
import java.time.Month
import kotlin.test.Test
import kotlin.test.assertEquals
class RecurrenceTest {
#Test
fun testInstants() {
val r = Recurrence("#daily")
val from = LocalDate.of(2021, Month.JANUARY, 1)
val forDays = 31
val instants = r.instants(from, from.plusDays(forDays.toLong())).toList()
assertEquals(forDays, instants.size)
(1..forDays).forEach {
assertEquals(from.plusDays(it.toLong() - 1).atStartOfDay(), instants[it - 1])
}
}
}
If I reimplement by building an ArrayList instead, it works as expected:
// new collection-based methods in Recurrence
fun instantsList(
fromInclusive: LocalDate = LocalDate.now(),
toExclusive: LocalDate = fromInclusive.plusMonths(1)
): List<LocalDateTime> = instantsList(fromInclusive.atStartOfDay(), toExclusive.atStartOfDay())
fun instantsList(
fromInclusive: LocalDateTime = LocalDateTime.now(),
toExclusive: LocalDateTime = fromInclusive.plusMonths(1)
): List<LocalDateTime> {
val list = arrayListOf<LocalDateTime>()
var it = cron.next(fromInclusive.minusNanos(1))
while (it !== null) {
if (it.isBefore(toExclusive)) {
list.add(it)
it = cron.next(it)
} else {
break
}
}
return list
}
The one line to change in the test is to use the new method:
val instants = r.instantsList(from, from.plusDays(forDays.toLong()))
Why is the sequence-based implementation returning me one more element than the list-based one?
If I read your code correctly, in list implementation you check if it.isBefore(toExclusive) and only then you add it to the list. In sequence implementation you do the same check it.isBefore(toExclusive) and then you add next item to the sequence.
Similar with the first item. In list implementation you check if cron.next(fromInclusive.minusNanos(1)) meets the requirement. In sequence implementation you always add it.
Thanks, #broot -- you spotted the issue. Just took another set of eyeballs. Correct sequence implementation is
fun instants(
fromInclusive: LocalDateTime = LocalDateTime.now(),
toExclusive: LocalDateTime = fromInclusive.plusMonths(1)
): Sequence<LocalDateTime> {
val seed = cron.next(fromInclusive.minusNanos(1))
return generateSequence(seed) {
val next = cron.next(it)
if (next.isBefore(toExclusive)) {
next
} else {
null
}
}
}

Use Kotlin's data class in service-proxy of Vert.x

I'm trying to pass data class to the service-proxy of Vert.x like this:
data class Entity(val field: String)
#ProxyGen
#VertxGen
public interface DatabaseService {
DatabaseService createEntity(Entity entity, Handler<AsyncResult<Void>> resultHandler);
}
However, the service-proxy requires a DataObject as the parameter type.
Below are what I've tried so far.
First, I rewrite the data class as:
#DataObject
data class Entity(val field: String) {
constructor(json: JsonObject) : this(
json.getString("field")
)
fun toJson(): JsonObject = JsonObject.mapFrom(this)
}
Although this works, the code is redundant, so I tried the kapt with the following generator:
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(ProxyDataObject::class.java).forEach { el ->
val className = el.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(el).toString()
val filename = "Proxy$className"
val classBuilder = TypeSpec.classBuilder(filename)
val primaryConstructorBuilder = FunSpec.constructorBuilder()
val secondaryConstructorBuilder = FunSpec.constructorBuilder().addParameter("json", JsonObject::class)
val secondaryConstructorCodeBlocks = mutableListOf<CodeBlock>()
el.enclosedElements.forEach {
if (it.kind == ElementKind.FIELD) {
val name = it.simpleName.toString()
val kClass = getClass(it) // get the corresponding Kotlin class
val jsonTypeName = getJsonTypeName(it) // get the corresponding type name in methods of JsonObject
classBuilder.addProperty(PropertySpec.builder(name, kClass).initializer(name).build())
primaryConstructorBuilder.addParameter(name, kClass)
secondaryConstructorCodeBlocks.add(CodeBlock.of("json.get$jsonTypeName(\"$name\")"))
}
}
secondaryConstructorBuilder.callThisConstructor(secondaryConstructorCodeBlocks)
classBuilder
.addAnnotation(DataObject::class)
.addModifiers(KModifier.DATA)
.primaryConstructor(primaryConstructorBuilder.build())
.addFunction(secondaryConstructorBuilder.build())
.addFunction(
FunSpec.builder("toJson").returns(JsonObject::class).addStatement("return JsonObject.mapFrom(this)").build()
)
val generatedFile = FileSpec.builder(pack, filename).addType(classBuilder.build()).build()
generatedFile.writeTo(processingEnv.filer)
}
return true
}
Then I can get the correct generated file by simply writing the original data class, but when I execute the building after cleaning, I still get the following error:
Could not generate model for DatabaseService#createEntity(ProxyEntity,io.vertx.core.Handler<io.vertx.core.AsyncResult<java.lang.Void>>): type ProxyEntity is not legal for use for a parameter in proxy
It seems that the generated annotation #DataObject is not processed.
So what should I do? Is there a better solution?

I can't generate a class using Kotlin processor

I'm implementing a processor to generate kotlin code using custom annotations. The problem is that I cannot find a way to relate the annotation to the field it was declared for, and I cannot find a way to understand if a field is of a nullable type. The processor doesn't succeed to generate the code because the getAnnotationsByType doesn't return the annotations for the current field (the list it's empty). Not even the order is good, fields are passed first and the annotations after all the fields.
package it.kfi.xml.binding.processor
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.*
import it.kfi.xml.binding.annotations.XmlClass
import it.kfi.xml.binding.annotations.XmlProperty
import java.io.File
import java.lang.reflect.Type
import javax.annotation.Nullable
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.type.NullType
import javax.lang.model.type.TypeMirror
import javax.print.DocFlavor
import javax.tools.Diagnostic
import kotlin.reflect.KClass
import kotlin.reflect.full.createType
#AutoService(Processor::class)
class XmlBinder : AbstractProcessor() {
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(XmlClass::class.java.name)
}
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(XmlClass::class.java)
.forEach {
if (it.kind != ElementKind.CLASS) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated")
return true
}
processClass(it)
}
return false
}
private fun processClass(element: Element) {
val className = element.simpleName.toString() + "Model"
val packageName = processingEnv.elementUtils.getPackageOf(element).toString()
val classBuilder = TypeSpec.classBuilder(className)
classBuilder.addModifiers(KModifier.PUBLIC)
val initFromXml = FunSpec.builder("initFromXml")
initFromXml.addModifiers(KModifier.PUBLIC)
initFromXml.addParameter(ParameterSpec.builder("xml", String::class).build())
val properties = element.enclosedElements
var x: Int = 1
//Look for elements annotated with XmlField and add those elements to the generated class
for (property in properties) {
val annotation = property.getAnnotationsByType(XmlProperty::class.java)
val v = 10
classBuilder.addProperty(PropertySpec.varBuilder(property.simpleName.toString(), String::class, KModifier.PUBLIC).initializer(v.toString()).build())
initFromXml.addStatement("this.${property.simpleName} = \"${v.toString()}\"")
}
classBuilder.addFunction(initFromXml.build())
val fileName = "kfi_generated_$className"
val file = FileSpec.builder(packageName, fileName).addType(classBuilder.build()).build()
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
file.writeTo(File(kaptKotlinGeneratedDir))
}
}
Can anyone help me found a way to relate annotations to their fields or properties ?