How to register custom configuration properties converter #ConfigurationPropertiesBinding - spring

In Spring Boot project, the configuration in the YML file can be automatically converted to an #ConfigurationProperties annotated bean. But I need to override behavior to make non standard conversion because I inject value from environmental variable (which is a sting) but it should be AS map.
There is error
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'app.push.firebase.application-keys' to java.util.Map<com.example.services.push.service.api.model.kafka.Application, java.lang.String>:
Property: app.push.firebase.application-keys
Value: "{"applicationOne": "api=key-one","applicationTwo": "api=key-two"}"
Origin: class path resource [application-local.yml] - 47:25
Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [java.util.Map<com.example.services.push.service.api.model.kafka.Application, java.lang.String>]
My application.yml
app:
use-dev-apns: true
kafka.consumer.group: 'local'
push:
errorCallbackUrl: 'callback-url'
firebase:
applicationKeys: '{"applicationOne": "api=key-one","applicationTwo": "api=key-two"}'
defaultKey: 'api-key'
My property class
#ConstructorBinding
#ConfigurationProperties("app.push")
data class PushProperties(
val errorCallbackUrl: String,
val firebase: FirebaseProperties
)
data class FirebaseProperties(
val applicationKeys: Map<Application,String>,
val defaultKey: String
)
And custom converter
#ConfigurationPropertiesBinding
#Component
class StringToMapConverter: Converter<String, Map<Application, String>> {
override fun convert(source: String): Map<Application, String> {
try {
val map = BasicJsonParser().parseMap(source) as Map<String, String>
return map.mapKeys { Application.valueOf(it.key.uppercase()) }
} catch (e: JsonParseException) {
throw Exception("app.callback-mappings property is invalid. Must be a JSON object string")
}
}
}
What could be the problem?
Custom converter bind data from string to Map<Application, String>

I have solved the problem.
The problem was that Kotlin Map class does not match the java.util.Map class.
When I change Kotlin Map class to MutableMap class everything works correctly

Related

Spring #Value injection conversion not working in #SpringJUnitConfig test

I couldn't find a way to make the automatic value conversion when injecting #Value work when using #SpringJUnitConfig test.
For example, for the given code:
#SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
#Component
class ValueConvertInjection(#Value("2ms") val duration : java.time.Duration) {
#PostConstruct
fun init() = println("Duration converted: $duration")
}
The given test will fail:
#SpringJUnitConfig(classes = [ValueConvertInjection::class])
class JUnitValueConvertInjectionTest {
#Autowired
lateinit var vi :ValueConvertInjection
#Test
fun test() = assert(vi.duration == Duration.ofMillis(2))
}
with the following exception:
...
Caused by: org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'java.time.Duration'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.time.Duration': no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:76)
... 85 more
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.time.Duration': no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:262)
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)
... 89 more
I couldn't find what I should add to the context so any of the Spring conversions will be registered and picked.
Spring Boot intializes the context with a conversion service, which is what you need:
public class ConversionConfiguration {
#Bean
fun conversionService() = ApplicationConversionService.getSharedInstance()
}
#SpringJUnitConfig(classes = [
ValueConvertInjection::class,
ConversionConfguration::class
]) {
class JUnitValueConvertInjectionTest {
#Autowired
lateinit var vi: ValueConvertInjection
#Test
fun test() = assert(vi.duration == Duration.ofMillis(2))
}
to use the duration class, can try as below
#Component
#ConfigurationProperties(prefix="time")
class ValueConvertInjection {
Duration val = Duration.ofSeconds(60);
#PostConstruct
fun init() = println("Duration converted: $duration")
}
and in your src/test/resources,create application.yml
time:
val:
2ms

Spring boot configuration properties validation not working

Trying to implement configuration properties validation for immutable beans as described in Spring boot docs:
#Validated
#ConstructorBinding
I'm using Spring boot 2.4.0.
Sample immutable properties class:
#Validated
#ConstructorBinding
#ConfigurationProperties("prefix")
public class Props {
private final String suffix;
public Props(#NotBlank String suffix) {
this.suffix = suffix;
}
public String getSuffix() {
return suffix;
}
#Override
public String toString() {
return "Props [suffix=" + suffix + "]";
}
}
For a test, I did this:
System.setProperty("prefix.suffix", "value");
and I get the correct binding, i.e.:
Props [suffix=value]
However, by making a typo in the property name (added 1 to the property name):
System.setProperty("prefix.suffix1", "value");
I get this:
Props [suffix=null]
I see validation activated in the logs:
HV000001: Hibernate Validator 6.1.6.Final
Form their docs, it is supposed to do constructor parameter validation:
As of Bean Validation 1.1, constraints can not only be applied to JavaBeans and their properties, but also to the parameters and return values of the methods and constructors of any Java type.
Why is #NotBlank (full import: import javax.validation.constraints.NotBlank;) not causing validation exceptions?

Trouble with Spring #ConfiurationProperties extending Mao

I am trying to load come properties from config using the Spring #Configuration & #ConfigurationProperties combination. I have a POJO that extends HashMap<Integer, String> and has a single variable inside. (See MyConfigPojo below). From the .yaml file, I have the same shape. However, when booting up the app, I get an error when trying to parse the string for the defaultValue into an Integer.
#Configuration
#ConfigurationPropeties(prefix = "my-config")
public class MyConfigPojo extends HashMap<Integer, String> {
private String defaultValue;
private String getValueForIdOrDefault(int id); // this.get(id) OR ELSE defaultValue
}
In config I have this:
myConfig:
1: "my first value"
23: "my 23rd value"
defaultValue: "cheese"
Which results in a
APPLICATION FAILED TO START
Description:
Failed to bind properties under 'myPackage' to MyConfigPojo:
Property: myConfig[1]
Value: cheese
I thought that was weird, so I turned on TRACE logs and found this:
99 common frames omittedCaused by: java.lang.NumberFormatException: For input string: "defaultValue" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
Is there a way to intercept reading this file or to tell Spring how to set the values properly?
You are trying to read a Map from .yml file, for that, you need #ConfigurationProperties annotation enough, don't need #Configuration annotation(if using spring boot). If you are using spring-framework, you need #Configuration also.
Use this example:
myConfig:
map:
1: "my first value"
23: "my 23rd value"
defaultValue: "cheese"
MyConfigPojo.java
package com.org;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
#Configuration
#ConfigurationProperties(prefix = "my-config")
public class MyConfigPojo {
private HashMap<String, String> map;
public HashMap<String, String> getMap() {
return map;
}
public void setMap(HashMap<String, String> map) {
this.map = map;
}
}
Now, you can Autowire this MyConfigPojo class anywhere(for instance, in controller class) and read those map keys and values.
#Autowired
MyConfigPojo pojo;
Now, you have considered keys & Values as String datatype for that Map, you will not get NumberFormatException.

Using Scala classes as DTOs in Spring MVC

In my project I'm using Spring + Scala.
Some of my Spring MVC controllers uses Spring feature for binding incoming HTTP parameters to DTO object. Like this:
#RequestMapping(value = Array("/", ""), method = Array(RequestMethod.POST))
def saveProduct(dto: MyDto): Iterable[MyDto] = {...}
And MyDto is simple scala class:
class MyDto extends Serializable {
#BeanProperty var id : Long = _
#BeanProperty var name: String = _
}
My problem is that I'm getting exceptions when trying to use Scala Option class for fields in MyDto:
class MyDto extends Serializable {
#BeanProperty var id : Option[Long] = None
#BeanProperty var name: Option[String] = None
}
Exception message is:
Failed to convert property value of type 'java.lang.String' to required type 'scala.Option' for property 'name';
What I can do to use Scala Options as type if fields in MyDto?
I am not a Scala expert, but here is one way:
Create a converter, along these lines:
import org.springframework.core.convert.converter.Converter
class TypeToOptionOfTypeConverter[T] extends Converter[T, Option[T]] {
override def convert(source: T): Option[T] = {
Some(source)
}
}
Register this converter with Spring MVC:
class WebConfig extends WebMvcConfigurerAdapter {
override def addFormatters(registry: FormatterRegistry): Unit = {
registry.addConverter(classOf[String], classOf[Option[String]], new TypeToOptionOfTypeConverter[String])
registry.addConverter(classOf[Long], classOf[Option[Long]], new TypeToOptionOfTypeConverter[Long])
}
}
That should be it, now your DTO should get cleanly mapped.
Spring has support for converting types using converters with its data binding. You will need to implement the converter so that Spring knows how to convert, for example, String to Option[String].
See:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html#core-convert

Is it possible to have non String type values in a Spring Environment PropertySource

I'm trying to add ConfigSlurper's ConfigObjectinto an application context Environment in order to provide configuration values to the context.
The MapPropertySorce itself only expects its values to be of type Object only. But in the end property resolution fails as the EnvironmentAccessor will try to cast each ConfigObject to String.
So basically the question is, is there support for non String property resource values? Any supporting classes there (different EnvironmentAccessor?)
class ConfigSlurperLearningSpec extends Specification {
def configurationResource = new ClassPathResource("/META-INF/de.troi/application.configuration")
ConfigObject configuration = new ConfigSlurper().parse(configurationResource.getURL())
def "use as application context property source"() {
expect:
AnnotationConfigApplicationContext context= new AnnotationConfigApplicationContext()
context.register(PropertySourceInjection)
context.getEnvironment().getPropertySources().addLast(new MapPropertySource("mapConfiguration", configuration))
context.refresh()
String configuredValue=context.getBean('testBean')
configuredValue=='create'
}
}
#Configuration
class PropertySourceInjection {
#Value("#{environment['entityManagerFactory']['jpaPropertyMap']['hibernate.hbm2ddl.auto']}")
Object hibernateHbm2ddlAuto;
#Bean
String testBean() {
return new String(hibernateHbm2ddlAuto.toString())
}
}
It is definitely possible to inject non-string objects via ConfigSlurper.
Take a look at (shameless plug) https://github.com/ctzen/slurper-configuration

Resources