Spring custom #ConditionalOnProperty annotation can not use #AliasFor - spring

I want to create FeatureFlag annotation for my project to avoid code repetitions.
I created a new annotation called FeatureFlag. I decorated with ConditionalOnProperty annotation with the generic prefix foo.features. I add new fields to the annotation, which is AliasFor the ConditionalOnProperty fields. As far as I know, the following code should work, but it does not. I also tested the aliasing on the Profile annotation and that is working.
import io.kotlintest.shouldBe
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.AliasFor
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
#ConditionalOnProperty(prefix = "foo.features")
annotation class FeatureFlag(
#get:AliasFor(annotation = ConditionalOnProperty::class, value = "name") val feature: String,
#get:AliasFor(annotation = ConditionalOnProperty::class, value = "havingValue") val enabled: String = "true"
)
#SpringBootTest(
properties = ["foo.features.dummy: true"],
classes = [FeatureFlagTest.FeatureFlagTestConfiguration::class]
)
class FeatureFlagTest(private val applicationContext: ApplicationContext) {
#Configuration
class FeatureFlagTestConfiguration {
#Bean
#FeatureFlag(feature = "dummy")
fun positive(): String = "positive"
#Bean
#FeatureFlag(feature = "dummy", enabled = "false")
fun negative(): String = "negative"
}
#Test
fun `test`() {
applicationContext.getBean(String::class.java) shouldBe "positive"
}
}
When I running the test case I get the following error:
java.lang.IllegalStateException: The name or value attribute of #ConditionalOnProperty must be specified
(The FeatureFlag annotation should contain the value of the name field.)
Can you help, what did I wrong? Is it a bug in the framework?

In addition to the prefix attribute, you also need to define the name or value attribute in the ConditionalOnProperty annotation in order for it to work.
Have a look here to see the details of the annotation you're using: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.html

Related

Kotlin spring boot #RequestBody validation is not triggered

I have a problem on a project with validating #RequestBody by using
implementation("org.springframework.boot:spring-boot-starter-validation")
My DTO looks like this:
import javax.validation.constraints.Email
import javax.validation.constraints.Pattern
class LoginDto(
#Email
val email: String,
#Pattern(regexp = Constants.PASSWORD_REGEX)
val password: String
)
And Controller looks like this:
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import javax.validation.Valid
#RestController
#Validated
class AuthController(private val authService: AuthService) {
#PostMapping("login")
fun login(#Valid #RequestBody loginDto: LoginDto): LoginResponse {
return authService.login(loginDto)
}
...
}
And there is no error from validation, if I try to pass invalid data:
{
"password":"hello",
"email":"dfdfdfdf"
}
I get no error
I use Exposed instead of jpa but I think it's not related to the problem
You should change the annotations of #email and #Pattern to #field:Email and #field:Pattern for example.
The reason for this is twofold, on the one hand you place the annotations on Kotlin properties, and Kotlin properties kan be accessed in a variety of ways. Therefore, you need to specify how you want to access the property to apply the annotation on. On the other hand, the annotations have a set of predefined targets. You can inspect the annotation to see for example that it has a target of field. That's why we can use the #field:Pattern and #field:Email.
This is a key difference with java, where you have have distinct getters, setters, and fields amongst others.

spring-data-neo4j v6: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]

Situation
I'm migrating a kotlin spring data neo4j application from spring-data-neo4j version 5.2.0.RELEASE to version 6.0.11.
The original application has several Repository interfaces with custom queries which take some DTO as a parameter, and use the various DTO fields to construct the query. All those types of queries currently fail with
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]
The reference documentation for spring-data-neo4j v6 only provides examples where parameters passed to custom query methods of a #Repository interface are of the same type as the #Node class associated with that repository. The documentation does not explicitly state that only parameters of the Node class are allowed.
Question
Is there any way to pass an arbitrary DTO (not being a #Node class) to a custom query method in a #Repository interface in spring-data-neo4j v6 like it was possible in v5?
Code samples
Example node entity
#Node
data class MyEntity(
#Id
val attr1: String,
val attr2: String,
val attr3: String
)
Example DTO
data class MyDTO(
val field1: String,
val field2: String
)
Example Repository interface
#Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
// ConverterNotFoundException is thrown when this method is called
#Query("MATCH (e:MyEntity {attr1: {0}.field1}) " +
"CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: {0}.field2))")
fun doSomethingWithDto(dto: MyDTO)
}
Solutions tried so far
Annotate DTO as if it were a Node entity
Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters
Mapped entities (everything with a #Node) passed as parameter to a
function that is annotated with a custom query will be turned into a
nested map.
#Node
data class MyDTO(
#Id
val field1: String,
val field2: String
)
Replace {0} with $0 in custom query
Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters
You do this exactly the same way as in a standard Cypher query issued
in the Neo4j Browser or the Cypher-Shell, with the $ syntax (from
Neo4j 4.0 on upwards, the old {foo} syntax for Cypher parameters has
been removed from the database).
...
[In the given listing] we are referring to the parameter by its name.
You can also use $0 etc. instead.
#Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
// ConverterNotFoundException is thrown when this method is called
#Query("MATCH (e:MyEntity {attr1: $0.field1}) " +
"CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: $0.field2))")
fun doSomethingWithDto(dto: MyDTO)
}
Details
spring-boot-starter: v2.4.10
spring-data-neo4j: v6.0.12
neo4j-java-driver: v4.1.4
Neo4j server version: v3.5.29
RTFM Custom conversions ...
Found the solution myself. Hopefully someone else may benefit from this as well.
Solution
Create a custom converter
import mypackage.model.*
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.neo4j.driver.Value
import org.neo4j.driver.Values
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
import java.util.HashSet
class DtoToNeo4jValueConverter : GenericConverter {
override fun getConvertibleTypes(): Set<ConvertiblePair>? {
val convertiblePairs: MutableSet<ConvertiblePair> = HashSet()
convertiblePairs.add(ConvertiblePair(MyDTO::class.java, Value::class.java))
return convertiblePairs
}
override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor?): Any? {
return if (MyDTO::class.java.isAssignableFrom(sourceType.type)) {
// generic way of converting an object into a map
val dataclassAsMap = jacksonObjectMapper().convertValue(source as MyDTO, object :
TypeReference<Map<String, Any>>() {})
Values.value(dataclassAsMap)
} else null
}
}
Register custom converter in config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.neo4j.core.convert.Neo4jConversions
import org.springframework.core.convert.converter.GenericConverter
import java.util.*
#Configuration
class MyNeo4jConfig {
#Bean
override fun neo4jConversions(): Neo4jConversions? {
val additionalConverters: Set<GenericConverter?> = Collections.singleton(DtoToNeo4jValueConverter())
return Neo4jConversions(additionalConverters)
}
}
It's ridiculous that the framework would force you to write a custom converter for this. I made a #Transient object in my overridden User class for a limited set of update-able user profile fields, and I'm encountering the same error. I guess I will just have to break up the object into its component String fields in the method params to get around this problem. What a mess.
#Query("MATCH (u:User) WHERE u.username = :#{#username} SET u.firstName = :#{#up.firstName},u.lastName = :#{#up.firstName},u.intro = :#{#up.intro} RETURN u")
Mono<User> update(#Param("username") String username,#Param("up") UserProfile up);
No converter found capable of converting from type [...UserProfile] to type [org.neo4j.driver.Value]

Javax Validation Custom enum constrains in Kotlin

I'm trying to create a custom annotation and validator to use in conjunction with the javax validation Api and I'm having trouble access the values of an enum.
The objective of the annotation and the validator is validate if an input data is present within the enum values.
This is the annotation class
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.reflect.KClass
#kotlin.annotation.Target(
AnnotationTarget.FIELD,
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MustBeDocumented
#Constraint(validatedBy = [ValueOfEnumValidator::class])
annotation class ValueOfEnum(
val enumClass: KClass<Enum<*>>,
val message: String ="",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
This is the validator implementation
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class ValueOfEnumValidator: ConstraintValidator<ValueOfEnum, CharSequence> {
private val acceptedValues: MutableList<String> = mutableListOf()
override fun initialize(constraintAnnotation: ValueOfEnum) {
super.initialize(constraintAnnotation)
acceptedValues.addAll(constraintAnnotation.enumClass.java
.enumConstants
.map {it.name}
)
}
override fun isValid(value: CharSequence?, context: ConstraintValidatorContext): Boolean {
return if (value == null) {
true
} else acceptedValues.contains(value.toString())
}
}
I'm aiming to use annotation like this:
#field:ValueOfEnum(enumClass = SortDirectionEnum::class, message = "{variants.sorted.sort.direction.not.valid}")
var sortDirection:String?=
But my IDE is reporting me the following error in the enumClass parameter
Type mismatch.
Required:KClass<Enum<*>>
Found:KClass<SortDirectionEnum>
How can I make the annotation generic enough to support different enums, and fix this issue ?
You are restricting enumClass to instances of Enum<*>, allowing Enum instances (Enum is an abstract class though, so nothing can be used) with all types of data, you however want to also allow child classes of Enum, which can be achieved with the out keyword there.
val enumClass: KClass<out Enum<*>>,
https://kotlinlang.org/docs/generics.html

This annotation is not applicable to target 'local variable

I want to get value from application.yml, but I got "This annotation is not applicable to target 'local variable" for this part,how to solve this problem?
#Value("\${aws.secretsManager.secretName}")
val secretName: String? = ""
#Configuration
#EnableConfigurationProperties
#ConfigurationProperties
fun getSecret() {
#Value("\${aws.secretsManager.secretName}")
val secretName: String? = ""
val region = "us-west-2"
val logger: Logger = LoggerFactory.getLogger(GetSecretConfig::class.java)
// Create a Secrets Manager client
val client = AWSSecretsManagerClientBuilder.standard().withRegion(region).build()
val getSecretValueRequest = GetSecretValueRequest().withSecretId(secretName)
var getSecretValueResult: GetSecretValueResult? = try {
client.getSecretValue(getSecretValueRequest)
}
}
application.yml
aws:
secretsManager:
secretName: "test-mvp"
region: "us-west-2"
user: "root"
password: "root"
From the #Value javadoc:
Annotation used at the field or method/constructor parameter level that indicates a default value expression for the annotated element.
The #Value annotation is defined as follow:
#Target(value = {FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
#Retention(value = RUNTIME)
#Documented
public #interface Value
As you see by the #Target, the #Value annotation it's not intended to be used in a LOCAL_VARIABLE.
The solution is to define the secretName variable outside of the function - as a field of the class.
Workaround logic here:
(Using Java 8 though - shouldn't matter anyways)
Create a configuration class annotated with #Configuration like so:
#Configuration
public class ApplicationSecretsConfig {
public ApplicationSecretsConfig(){}
#Value("${aws.secretsManager.secretName}")
private String secretName;
public String getSecretName(){
return secretName;
}
}
Then in your class, autowire the SecretsConfig dependency and get the value of secretName using its getter.
// class initialization done here
...
#Autowired
ApplicationSecretsConfig applicationSecretsConfig
public String getSecret() {
String secret = applicationSecretsConfig.getSecretName();
// continue your logic
...
}
Hopefully that helps someone.
there is no need to do a custom implementation for fetch secrets. Spring provides it, using spring-cloud-starter-aws-secrets-manager-config dependency, just need to do an small config:
spring.config.import=aws-secretsmanager:my-secret
there is working sample on documentation:
https://github.com/awspring/spring-cloud-aws/tree/main/spring-cloud-aws-samples/spring-cloud-aws-parameter-store-sample
and here you could find a db working too:
https://github.com/nekperu15739/aws-secrets-manager

Spring Validation of JSON - Why do I need to add `#field`

I've finally made some progress on Spring validation (on a JSON object coming in from RabbitMQ).
However there are a couple of things I don't understand:
In the documentation, it states I can just use the annotation #NotBlank then in my method I use the annotation #Valid. However I find this wasn't doing anything. So instead I did #field:NotBlank and it worked together with the following - why did this #field do the trick?
#JsonIgnoreProperties(ignoreUnknown = true)
data class MyModel (
#field:NotBlank(message = "ID cannot be blank")
val id : String = "",
#field:NotBlank(message = "s3FilePath cannot be blank")
val s3FilePath : String = ""
)
Then the function using this model:
#Service
class Listener {
#RabbitListener(queues = ["\${newsong.queue}"])
fun received(data: MyModel) {
val factory = Validation.buildDefaultValidatorFactory()
val validator = factory.validator
val validate = validator.validate(data)
// Then this `validate` will return an array of validation errors
println(validate)
}
}
Correct me if I'm wrong however I assumed just using #Valid and this point fun received(#Valid data: MyModel) it would just throw some exception for me to catch - any idea based on my code why this could have been?
Any advice/help would be appreciated.
Thanks.
Here are the imports:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import javax.validation.*
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.amqp.rabbit.annotation.RabbitListener
import javax.validation.constraints.NotBlank
Quoting Kotlin's documentation for annotations:
When you're annotating a property or a primary constructor parameter, there are multiple Java elements which are generated from the corresponding Kotlin element, and therefore multiple possible locations for the annotation in the generated Java bytecode. To specify how exactly the annotation should be generated, use the following syntax:
class Example(#field:Ann val foo, // annotate Java field
#get:Ann val bar, // annotate Java getter
#param:Ann val quux) // annotate Java constructor parameter
So, until explicitly mention what you are annotating (field, getter or something else) in Kotlin class constructor, it won't automatically know where you want to put that annotation.

Resources