How to read List of Kotlin Pairs from application.yml - spring-boot

I am working on a Spring Boot project in Kotlin. I would like to read a list of <String,Int> pairs from application.yml file to a mutablelist. The type of the list is as follows.
#Configuration
#EnableAutoConfiguration
#ConfigurationProperties(prefix = "user-parameters")
class UserParameters {
/**
* Owners of the knapsacks and their capacity.
*/
var knapsacks = mutableListOf<Pair<String, Int>>()
init {
...
}
}
In the application.yml file, I tried to use the following configuration.
...
user-parameters:
knapsacks:
- "[James]": 102
- "[Mary]": 185
However, this fails with the following error.
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target [Bindable#3a7b2e2 type = java.util.List<kotlin.Pair<java.lang.String, java.lang.Integer>>, value = 'provided', annotations = array<Annotation>[[empty]]] failed:
Property: user-parameters.knapsacks[0][James]
Value: 102
Origin: class path resource [application.yml] - 28:17
Reason: The elements [user-parameters.knapsacks[0][James],user-parameters.knapsacks[1][Mary]] were left unbound.
Property: user-parameters.knapsacks[1][Mary]
Value: 185
Origin: class path resource [application.yml] - 29:16
Reason: The elements [user-parameters.knapsacks[0][James],user-parameters.knapsacks[1][Mary]] were left unbound.
Action:
Update your application's configuration
The code works if I change the type to Map<String, Int> but I need Pair<String, Int> due to implementation details.
I have also tried the following annotations but to no avail.
...
user-parameters:
knapsacks:
- "[James]": 102
"[Mary]": 185
...
user-parameters:
knapsacks:
"[James]": 102
"[Mary]": 185
...
user-parameters:
knapsacks:
- { "[James]": 102 }
- { "[Mary]": 185 }
How can I achieve reading pairs of String,Int to a mutable list from application.yml?

As far as I know Spring Boot does not support Pair of the kotlin-stdlib out-of-the-box.
Spring cannot know how to parse arbitrary classes in arbitrary ways. Spring does support classes following Bean conventions however.
Thus, there are at least three solutions to your problem:
Simply parse as Map as outlined in the question and convert your Map<String, Int> to a List<Pair<String, Int>>, e.g. by calling map.toList(), see the official documentation.
You may provide a custom getter for the list.
val knapsacksList: List<Pair<String, Int>>
get() { knapsacks.toList() }
Adjust your application.yml to conform to the form of Pair. A Pair is a simple data class having two values being first and second. If you provide these in the .yml-file, Spring can parse them accordingly.
Something along the lines of the following may work.
...
user-parameters:
knapsacks:
- first: "James"
second: 102"
- first: "Mary"
second: 185
Provide a custom Converter for your use case. There are resources online on how to achieve this, for example from baeldung.com.

Related

Problem with Protostream and UUID in Infinispan 13.0.0.Final

I'm using Infinispan 13.0.0.final with the default marshaller (protobuf). When I try to use UUID fields in my datatypes
data class CounterState(
#get:ProtoField(number = 1) var index: Long? = null,
#get:ProtoField(number = 2) var uuid: UUID? = null
)
I get the following error at build time:
.../gradle-kotlin-protobuf/build/tmp/kapt3/stubs/main/io/radiosphere/ProtoSchema.java:8: error: org.infinispan.protostream.annotations.ProtoSchemaBuilderException: The class java.util.UUID must be instantiable using an accessible no-argument constructor.
public abstract interface ProtoSchema extends org.infinispan.protostream.GeneratedSchema {
It seems like I'm not allowed to use UUID in my types unless I generate a protoschema for it, but since UUID is a class outside of my control I can't do this.
Previous questions on the topic have gotten the suggestion to use the JavaSerializationMarshaller, but I want to solve this while still using the Protostream Marshaller. It has also been suggested that this would be fixed in version 12.0.0 here.
An example of this not working can be found here. Note that this project will not build because of the annotation processing failing as mentioned above. If it would build the proof that it is working would be shown by running the main project (ie. not the tests).
The question becomes: What do I need to do to configure UUID to be usable in my protobuf marshalled classes in Infinispan 13? Both for embedded and for a program using the hotrod client?
EDIT:
Based on a given answer I have also tried doing the following:
#AutoProtoSchemaBuilder(
includeClasses = [UUIDAdapter::class, CounterState::class],
schemaPackageName = "tutorial")
interface ProtoSchema : GeneratedSchema {
}
This makes the build work, but when starting Quarkus I get the following error:
Caused by: org.infinispan.protostream.DescriptorParserException: Duplicate type id 1005 for type org.infinispan.protostream.commons.UUID. Already used by tutorial.UUID
at org.infinispan.protostream.descriptors.ResolutionContext.checkUniqueTypeId(ResolutionContext.java:151)
at org.infinispan.protostream.descriptors.ResolutionContext.addGenericDescriptor(ResolutionContext.java:97)
at org.infinispan.protostream.descriptors.FileDescriptor.collectDescriptors(FileDescriptor.java:313)
at org.infinispan.protostream.descriptors.FileDescriptor.resolveDependencies(FileDescriptor.java:245)
at org.infinispan.protostream.descriptors.FileDescriptor.resolveDependencies(FileDescriptor.java:210)
at org.infinispan.protostream.descriptors.ResolutionContext.resolve(ResolutionContext.java:57)
at org.infinispan.protostream.impl.SerializationContextImpl.registerProtoFiles(SerializationContextImpl.java:127)
at org.infinispan.protostream.types.java.CommonTypesSchema.registerSchema(CommonTypesSchema.java:49)
at org.infinispan.client.hotrod.RemoteCacheManager.registerSerializationContextInitializer(RemoteCacheManager.java:422)
at org.infinispan.client.hotrod.RemoteCacheManager.registerDefaultSchemas(RemoteCacheManager.java:437)
at org.infinispan.client.hotrod.RemoteCacheManager.initializeProtoStreamMarshaller(RemoteCacheManager.java:409)
at org.infinispan.client.hotrod.RemoteCacheManager.actualStart(RemoteCacheManager.java:365)
at org.infinispan.client.hotrod.RemoteCacheManager.start(RemoteCacheManager.java:334)
at org.infinispan.client.hotrod.RemoteCacheManager.<init>(RemoteCacheManager.java:192)
at org.infinispan.client.hotrod.RemoteCacheManager.<init>(RemoteCacheManager.java:149)
at io.quarkus.infinispan.client.runtime.InfinispanClientProducer.initialize(InfinispanClientProducer.java:68)
If I instead change to use dependsOn like this:
#AutoProtoSchemaBuilder(
includeClasses = [CounterState::class],
dependsOn = [org.infinispan.protostream.types.java.CommonTypes::class, org.infinispan.protostream.types.java.CommonContainerTypes::class],
schemaPackageName = "tutorial")
I'm back to the build failing with:
error: org.infinispan.protostream.annotations.ProtoSchemaBuilderException: The class java.util.UUID must be instantiable using an accessible no-argument constructor.
public abstract interface ProtoSchema extends org.infinispan.protostream.GeneratedSchema {
It seems to be like Quarkus and the Annotation processor are getting in each others way here when it comes to having a simple working solution for UUID marshalling.
You have to include the org.infinispan.protostream.types.java.util.UUIDAdapter class in your annotation:
#AutoProtoSchemaBuilder(includeClasses = [CounterState::class, UUIDAdapter::class] , schemaPackageName = "tutorial")
For more info, check the documentation page.

spring boot: unable to bind yaml list objects

I need to bind a list of POJO by yaml properties file but I was no luck to get it work.
My application.yml has the following lines:
printer:
printers:
- deviceNo: abc
key: 123
And PrinterProperties like this:
#Component
#ConfigurationProperties(prefix = "printer")
class PrinterProperties {
var printers: List<Printer> = listOf()
}
But the field printers was not poputated with the application.ymlvalue.
There was a samilar problem
I pushed a demo to a github repository to demostrate this problem.
As described in the Spring Boot docs at Externalized Configuration page, you can bind properties like that in your example as long they accomplish one of this conditions:
1) The class property has a setter
2) It's initialized with a mutable value.
listOf() will give you an unmutable value therefore it won't work.
Hope it helped! :)
From Andy Wilkinson's advice: the POJO should have a default constructor. So I changed the POJO with:
class Printer {
var deviceNo: String? = null
var key: String? = null
}
and it works now.
printer:
printers:
-
deviceNo: abc
key: 123
Your yaml file should like this.

How to modify prometheus exposed metric names using Actuator in Spring-boot 2

I am using Actuator in springboot 2 to expose /actuator/prometheus endpoint from which a prometheus instance will pull metrics.
Everithing works perfect except because I am in need of tweak the metric names. I mean not the suffix (_count, _total, _bucket, ...) which are meaningful for Prometheus but something like:
http_server_requests_seconds_count -> http_server_requests_count
http_server_requests_seconds_max -> latency_seconds_max
http_server_requests_seconds_sum -> latency_seconds_sum
http_server_requests_seconds_bucket -> latency_seconds_bucket
Is there any better approach to this?
P.S.
I know i can use
management.metrics.web.server.requests-metric-name=different
to get
different_seconds_count
different_seconds_max
different_seconds_sum
different_seconds_bucket
but it would be difficult to:
1º remove the _seconds suffix
2º use a different base name for only one of them
I am guessing i could write an alternative PrometheusRenameFilter but not sure how to configure it to the default registry.
you can override this method and update the naming convention:
#Configuration
public class MetricsConfiga {
#Bean
MeterRegistryCustomizer<MeterRegistry> configurer(String applicationName) {
return (registry) -> registry.config().namingConvention(new NamingConvention() {
#Override
public String name(String name, Meter.Type type, String baseUnit) {
return "PREFIX" + Arrays.stream(name1.split("\\."))
.filter(Objects::nonNull)
.collect(Collectors.joining("_"));
}
});
}
}
Now I know how I can customize the global registry:
e.g. to set a custom meter filter:
#Configuration
public class MetricsConfig {
#Bean
MeterRegistryCustomizer<MeterRegistry> metricsConfig() {
return registry -> registry.config().meterFilter(new CustomRenameFilter());
}
}
However, setting a custom rename filter in the registry only allow to rename the base metric name.
It does not act on the suffixes nor allow to act on specific metric belonging a set e.g. generated by the summary.
with a custom NamingConvention I can add suffixes to convention base name ... I could even alter existing suffixes or replace convention base name.
Finally please note that Histogram prometheus metric type expects the creation of
<basename>_bucket
<basename>_sum
<basename>_count
with those specific names so it might be incorrect to tweak the component in the way I want because that would be a diferent component.

ConfigurationProperties loading list from YML

I'm trying to load Configuration from YML. I can load value and I can also load list if these are comma seperated values. But i can't load a typical YML List.
Configuration Class
#Component
#PropertySource("classpath:routing.yml")
#ConfigurationProperties
class RoutingProperties(){
var angular = listOf("nothing")
var value: String = ""
}
Working routing.yml
angular: /init, /home
value: Hello World
Not Working routing.yml
angular:
- init
- home
value: Hello World
Why can't i load the second version of yml / do I have a syntaxt error?
ENV: Kotlin, Spring 2.0.0.M3
As #flyx say, #PropetySource not worked with yaml files. But in spring you may override almost everything :)
PropertySource has additional parameter: factory. It's possible to create your own PropertySourceFactory base on DefaultPropertySourceFactory
open class YamlPropertyLoaderFactory : DefaultPropertySourceFactory() {
override fun createPropertySource(name: String?, resource: EncodedResource?): org.springframework.core.env.PropertySource<*> {
if (resource == null)
return super.createPropertySource(name, resource)
return YamlPropertySourceLoader().load(resource.resource.filename, resource.resource, null)
}
}
And when use this factory in propertysource annotation:
#PropertySource("classpath:/routing.yml", factory = YamlPropertyLoaderFactory::class)
Last that you need is to initialized variable angular with mutableList
Full code sample:
#Component
#PropertySource("classpath:/routing.yml", factory = YamlPropertyLoaderFactory::class)
#ConfigurationProperties
open class RoutingProperties {
var angular = mutableListOf("nothing")
var value: String = ""
override fun toString(): String {
return "RoutingProperties(angular=$angular, value='$value')"
}
}
open class YamlPropertyLoaderFactory : DefaultPropertySourceFactory() {
override fun createPropertySource(name: String?, resource: EncodedResource?): org.springframework.core.env.PropertySource<*> {
if (resource == null)
return super.createPropertySource(name, resource)
return YamlPropertySourceLoader().load(resource.resource.filename, resource.resource, null)
}
}
#SpringBootApplication
#EnableAutoConfiguration(exclude = arrayOf(DataSourceAutoConfiguration::class))
open class Application {
companion object {
#JvmStatic
fun main(args: Array<String>) {
val context = SpringApplication.run(Application::class.java, *args)
val bean = context.getBean(RoutingProperties::class.java)
println(bean)
}
}
}
Kinda old post, i know. But i am at the very same topic right now.
As of now, it seems that PropertySource does indeed work with yaml Files. Given the restriction that it only allows for primitive types (it seems) and it cant handle nested elements. I'm probably gonna dig a bit deeper and update my answer accordingly, but as of now, the accepted answer seems like a functioning workaround.
Well, according to the docs, your YAML file will be rewritten into a property file. The first YAML file becomes:
angular=/init, /home
value=Hello World
While the second one becomes:
angular[0]=init
angular[1]=home
value=Hello World
These are obviously two very different things and therefore behave differently.
Moreover, later in the docs, it is stated that YAML does not even work with #PropertySource:
24.6.4 YAML shortcomings
YAML files can’t be loaded via the #PropertySource annotation. So in the case that you need to load values that way, you need to use a properties file.
That makes me kind of wonder why the first case works for you at all.
The docs say this about the generated …[index] properties:
To bind to properties like that using the Spring DataBinder utilities (which is what #ConfigurationProperties does) you need to have a property in the target bean of type java.util.List (or Set) and you either need to provide a setter, or initialize it with a mutable value, e.g. this will bind to the properties above
So, let's have a look at Kotlin docs: listOf returns a new read-only list of given elements. So the list is not mutable as required by the docs, which I assume is why it doesn't work. Try using a mutable list (since I have never used Kotlin, I cannot give you working code). Also try to declare it as java.util.List if that's possible in Kotlin.

Camel type converter fails: InvalidPayloadException: No body available of type

The application is based on OSGI.
I have a custom annotated converter:
package com.domain.bundle1.web.camel.converters;
import ...;
#Converter
public class FooTransferObjectConverter {
public FooTransferObjectConverter() {
}
#Converter
public static FooTransferObject toFooTransferObject(Foo foo, Exchange exchange) throws Exception {
// some magic
return fooTransferObject;
}
}
Also i declared package where it plased in TypeConverter file:
http://i.stack.imgur.com/U3QQH.png
which contains:
com.domain.bundle1.web.camel.converters
And camel-context file contains next code:
<log loggingLevel="INFO" message="Converting to FooTransferObject" />
<convertBodyTo type="com.domain.bundle2.model.FooTransferObject" />
<log loggingLevel="INFO" message="Converted!" />
Before converting, body of message is a Foo object.
But when process reaches converting, then throws an exception:
Failed delivery for (MessageId: ID-EPUALVIW0567-55536-1401106375216-26-5 on ExchangeId: ID-EPUALVIW0567-55536-1401106375216-26-6).
Exhausted after delivery attempt: 1 caught: org.apache.camel.InvalidPayloadException: No body available of type: com.domain.bundle2.model.FooTransferObject but has value: Foo{97, Wall, null, null} of type: com.domain.bundle3.model.Foo on: Message: Foo{97, Wall, null, null}.
Caused by: Error during type conversion from type: com.domain.bundle3.model.Foo to the required type: com.domain.bundle2.model.FooTransferObject with value Foo{97, Wall, null, null} due 6 counts of IllegalAnnotationExceptions. Exchange[Message: Foo{97, Wall, null, null}]. Caused by: [org.apache.camel.TypeConversionException - Error during type conversion from type: Foo{97, Wall, null, null} to the required type: com.domain.bundle2.model.FooTransferObjec with value....
then exception cached by custom handler,
and then I found this:
Caused by: javax.xml.bind.MarshalException
- with linked exception:
[com.sun.istack.internal.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML: freebaseball SpeedKick -> fr????f????tb??ll Sp????dK??ck -> free
football SpeedKick ]
at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:311)[:1.7.0_40]
at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:236)[:1.7.0_40]
at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:95)
at org.apache.camel.converter.jaxb.FallbackTypeConverter.marshall(FallbackTypeConverter.java:238)
at org.apache.camel.converter.jaxb.FallbackTypeConverter.convertTo(FallbackTypeConverter.java:95)
... 163 more
Caused by: com.sun.istack.internal.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML: freebaseball SpeedKick -> fr????f????tb??ll Sp????dK??c
k -> freebaseball SpeedKick
at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:237)[:1.7.0_40]
How do You think what's a problem? How can I see loaded converters in TypeConverterRegistry?
I have already solved my problem. FallbackTypeConverter started work, because camel didn't load my custom regular type converter.
I checked the map of converters in TypeConverterRegister in debug mode, and didn't find my FooTransferObjectConverter.
The problem was in file TypeConverter. I just added name of converter class to path and after that it loaded to registry.
com.domain.bundle1.web.camel.converters.FooTransferObjectConverter
Camel version in application - 2.11.1. In camel docs written next:
In Camel 2.8 we improved the type converter loader to support
specifying the FQN class name of the converter classes. This has the
advantage of avoiding having to scan packages for #Converter classes.
Instead it loads the #Converter class directly. This is a highly
recommend approach to use going forward.
But i tryed run the application from chapter 3 (from 'Camel in action' book) with custom converter. And file TypeConverter contained only from package path.

Resources