I have the following property list in my application.yml:
foo:
bar:
-
id: baz
item: value
// ...
Then I want to overwrite item value in tests using #DynamicPropertySource:
#DynamicPropertySource
#JvmStatic
#Suppress("unused")
fun setupProperties(registry: DynamicPropertyRegistry) {
registry.add("foo.bar[0].item") { "new value" }
}
But during the tests, I got all other properties set to nulls, with one element in bar array.
I guess that I'm not referring correctly to map entry in yaml file.
I wonder how I can do that?
It turns out that Spring Boot documentation states clearly:
When lists are configured in more than one place, overriding works by replacing the entire list.
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-complex-type-merge
This effectively means that I need to provide whole list item:
#DynamicPropertySource
#JvmStatic
#Suppress("unused")
fun setupProperties(registry: DynamicPropertyRegistry) {
registry.add("foo.bar[0].id") { "new baz" }
registry.add("foo.bar[0].item") { "new value" }
// ...
}
Related
Suppose I am currently authoring a Gradle plugin, and I have this extension:
abstract class MyExtension {
abstract val inputFiles: ConfigurableFileCollection
val names: Provider<List<String>> by lazy {
// Read input files to get a list of names
inputFiles.elements.map { ... }
}
abstract val instances: NamedDomainObjectContainer<MyType>
}
What I want to do in my plugin is to make sure that every name provided by names is registered in instances if not already registered. That is, something like this:
names.get().forEach {
if (!instances.names.contains(it)) {
instances.register(it) {
// Additional configuration goes here
}
}
}
I have heard that project.afterEvaluate() is an option, however I have also heard that it is a bad option that shouldn't be used. What would be a way to have this functionality?
You can use another DomainObjectContainer, which has various functionality to allow it to stay up-to-date with other objects being configured.
For instance you could have the extension class:
abstract class MyExtension {
abstract val inputFiles: ConfigurableFileCollection
abstract val names: DomainObjectSet<String>
abstract val instances: NamedDomainObjectContainer<MyType>
}
Then have in your plugin code:
val ext = project.extensions.create("MyExtension", MyExtension::class.java)
ext.names.all { eachName ->
ext.instances.register(eachName) {
// Additional config
}
}
ext.names.addAllLater(ext.inputFiles.elements.map { ... })
The lambda passed to all will be executed for each new name added to names, including all the files, so all the collections will stay current with each other.
Is there a way to validate a property value when the property is evaluated? I can't do it in the getter because that returns the Property object - I want the validation to run only when the actual value is calculated (i.e. I want to be lazy evaluation friendly).
They show extensions using the Property object here:
https://docs.gradle.org/current/userguide/lazy_configuration.html#connecting_properties_together
However, they don't explain how to do property validation when the value is calculated. Here is the snipet of code from the Gradle documentation provided example:
// A project extension
class MessageExtension {
// A configurable greeting
final Property<String> greeting
#javax.inject.Inject
MessageExtension(ObjectFactory objects) {
greeting = objects.property(String)
}
}
If I wanted to make sure the value of greeting was not equal to test, then how would I do that when it is evaluated?
For most use cases, it should be sufficient to just validate the property value once you resolve it in your task or in other internal parts of your plugin. Only a few extensions are actually designed to be consumed by other plugins or the build script.
Gradle does not provide some validation that can be attached to a property, however you can build this functionality on your own like in the example below:
class MessageExtension {
private final Property<String> _greeting
final Provider<String> greeting
#javax.inject.Inject
MessageExtension(ObjectFactory objects) {
_greeting = objects.property(String)
greeting = _greeting.map { value ->
if (value.equals('test'))
throw new RuntimeException('Invalid greeting')
return value
}
}
def setGreeting(String value) {
_greeting.set(value)
}
def setGreeting(Provider<String> value) {
_greeting.set(value)
}
}
project.extensions.create('message', MessageExtension)
message {
greeting = 'test'
}
println message.greeting.get()
I turned the Property into a backing field for a Provider that runs the validation when resolved. If you do not want to throw an exception, but just return an empty Provider, you may replace the map with a flatMap.
I'm using Kotlin reflection to check if attributes that have a certain annotation are null.
Given the following example:
data class DataClass(
#SomeRandomAnnotation
val otherAnnotated: String?,
val inner: InnerClass
)
data class AnotherDataClass(
#SomeRandomAnnotation
val annotatedProperty: String?,
val dataClass: DataClass
) {
fun checkCreditAnalysisConstrain() {
print(checkConstrain(this))
}
}
And the function that checks it:
fun checkConstrain(parentClass: Any): List<String> {
val filter = parentClass::class.memberProperties.filter {
if (memberIsDataClass(it)) checkConstrain(getMemberPropertyInstance(parentClass, it))
hasAnnotation(it) && propertyIsNull(it, parentClass)
}
return filter.map { formatResult(parentClass, it) }
}
The idea is that the function is going to iterate through the attributes of my classes checking if they have the annotation and checking if the value is null.
If the property is a data class, the code evaluates the properties of the childs, recursively.
After that, I map the results, transforming the KProperty's into a simple String that is human readable, containing the class name and the attribute name.
The problem is that the above code does not work as expected. The properties returned are only the properties from the first-level class.
If, instead of doing a filter, I just run a forEach and print the result, I get the expected attributes. So I'm pretty sure it's related to the recurring inside a filter.
Do you see any way of doing this in a more functional way? I'm just concerned I won't need a "temp" list and add values to the list and reset it afterwards.
Your function recursively calls itself, but does nothing with the returned list of that recursive call. That's why you only get results for the top-level class.
Also, in my opinion, you shouldn't rely on side effects happening from your filter call. It probably works, but the function's documentation does not provide a guarantee that it will be called exactly once per item in the collection. So there should be a separate for-loop to do the recursive calls, and the result should be added onto existing results.
fun checkConstrain(parent: Any): List<String> {
val memberProperties = parent::class.memberProperties
var result = memberProperties
.filter { hasAnnotation(it) && propertyIsNull(it, parent) }
.map { formatResult(parent, it) }
memberProperties.filter { memberIsDataClass(it) }
.mapNotNull { getMemberPropertyInstance(parent, it) }
.forEach { result += checkConstrain(it) }
return result
}
You didn't provide code for several of the functions you used. This is what I used for them:
val KProperty<*>.returnTypeClass get() = this.returnType.classifier as? KClass<*>
fun <T> memberIsDataClass(member: KProperty<T>) = member.returnTypeClass?.isData == true
fun <T> getMemberPropertyInstance(parent: Any, property: KProperty<T>) = property.getter.call(parent)
fun <T> hasAnnotation(property: KProperty<T>) = property.annotations.firstOrNull { it.annotationClass == SomeRandomAnnotation::class } != null
fun <T> propertyIsNull(property: KProperty<T>, parent: Any) = getMemberPropertyInstance(parent, property) == null
fun formatResult(parent: Any, property: KProperty<*>) = "$parent's property(${property.name}) is annotated with SomeRandomAnnotation and is null."
I'm writing a custom gradle plugin that needs to accept an arbitrary number of nested parameters from the buildscript. Something like:
myPlugin{
configObjects = [
{
name="objectA",
value=5,
},
{
name="objectB",
value=9,
}
]
}
...where the number of items in configObjects, and the the values inside them is defined in whatever buildscript is importing the plugin.
So in my plugin code, I create an extension...
val config = extensions.create("myPlugin", myPluginTaskConfiguration::class.java, project)
tasks {
register<myPluginTask>("myPlugin") {
configObjects= config.configObjects
}
}
and a class defining the structure of the data received through the extension:
open class myPluginTaskConfiguration(project: Project) {
#Input
#Option(option="configObjects", description = "list of configObjects")
var configObjects:List<ConfigObject>?=null
}
Gradle allows me to specify the outer type, but apparently not the inner members. Running my plugin task I get the following error:
class build_f42r2ugava4a351q5usw8u65g$_run_closure1$_closure5 cannot be cast to class com.myplugin.ConfigObject (build_f42r2ugava4a351q5usw8u65g$_run_closure1$_closure5 is in unnamed module of loader org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler$ScriptClassLoader #224ed88; com.myplugin.ConfigObject is in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader #72fe231e)
It's not clear to me what the type of the objects in the configObjects block is (well, apparently they're of type build_f42r2ugava4a351q5usw8u65g$_run_closure1$_closure5, but I don't think that's something I can use at author-time)
How can I take the list of items from my groovy buildscript, and convert them into typed objects in my plugin (preferably in a way that allows the IDE to provide suggestions/hints to users editing the buildscript)?
#Input and #Option are for tasks. From the looks of it, you are using them for extensions.
There is no need to need to pass in a project instance in the constructor of a Task. All Tasks have a reference to the Project they belong to https://docs.gradle.org/current/javadoc/org/gradle/api/Task.html#getProject--
With that said, full working example in Kotlin would be:
open class MyPluginTaskConfiguration #Inject constructor(objects: ObjectFactory) {
val configObjects: ListProperty<Map<*, *>> = objects.listProperty()
}
open class MyPluginTask : DefaultTask() {
#Input
#Option(option="configObjects", description = "list of configObjects")
val configObjects: ListProperty<Map<*, *>> = project.objects.listProperty()
#TaskAction
fun printMessage() {
configObjects.get().forEach {
println("$it")
}
}
}
val config = extensions.create("myPlugin", MyPluginTaskConfiguration::class.java)
configure<MyPluginTaskConfiguration> {
configObjects.set(listOf(
mapOf<String, Any>(
"name" to "objectA",
"value" to 5
),
mapOf<String, Any>(
"name" to "objectB",
"value" to 9
)
))
}
tasks.register("myPlugin", MyPluginTask::class) {
configObjects.set(config.configObjects)
}
Executing the above produces:
./gradlew myPlugin
> Task :myPlugin
{name=objectA, value=5}
{name=objectB, value=9}
Refer to below doc for more details:
https://docs.gradle.org/current/userguide/lazy_configuration.html
I have a Rulebook that contains Rules:
class Rulebook(val rules:MutableList<Rule>)
I have an ItemViewModel for it, as it's used in a multiply-nested selection UI.
class RulebookModel : ItemViewModel<Rulebook> {
val rulesProperty = bind // ... here's my problem
}
What is the correct binding to be able to initialize a tableview with the property?
A naive bind yields the wrong type:
val rulesProperty = bind(Rulebook::rules)
has type Property<MutableList<Rule>>, which tableview() doesn't take.
From another answer here I got Link
val rulesProperty = bind(Rulebook::rules) as ListProperty<Rule>
This yields the correct type, so we get through compilation, but at runtime I get this:
java.lang.ClassCastException: java.util.ArrayList cannot be cast to javafx.collections.ObservableList
Note: The RulebookModel does start life without an item in it yet. I've seen ArrayLists come from empty list factory calls before. Is that possibly my actual problem?
What is the correct way to perform this binding?
Your model needs to have a SimpleListProperty to bind into an itemViewModel
Here is some sample code for how to write the classes and a table view:
data class rule(val name: String, val def: String)
class RuleBookModel{
val rulesProperty = SimpleListProperty<rule>()
var rules by rulesProperty
}
class RuleBookViewModel: ItemViewModel<RuleBookModel>() {
val rules = bind(ruleBook::rulesProperty)
}
class TestView : View("Test View") {
val myRuleBook: RuleBookViewModel by inject()
init {
// adding a rule so the table doesn't look lonely
myRuleBook.rules.value.add(rule("test", "fuga"))
}
val name = textfield()
val definition = textfield()
override val root = vbox{
hbox {
label("Name")
add(name)
}
hbox {
label("Definition")
add(definition)
}
button("Add a rule").action{
myRuleBook.rules.value.add(rule(name.text, definition.text))
}
tableview(myRuleBook.rules) {
column("name", rule::name)
column("def", rule::def)
}
}
}