I asked this question on StackOverflow without any suggestion. Currently I worked around it by using Java file in Kotlin project. Here is the question:
I just want to port the following code from Java which uses Log4j (v2.8.2):
ConsoleAppender appender = ConsoleAppender.newBuilder().
withName(“ConsoleAppender”).build();
The problem is with newBuilder() method which is defined as some kind of recursive generic in log4j:
@PluginBuilderFactory
public static <B extends Builder<B>> B newBuilder() {
return new Builder<B>().asBuilder();
}
Java code infers the generic parameter automatically which is not the case for Kotlin. Is there any solution to call this method in Kotlin?
The code I have tried in Kotlin:
val appender =
ConsoleAppender.newBuilder().withName("ConsoleAppender").build()
It has the following error:
Error:(90, 48) Kotlin: Type inference failed: Not enough information to infer
parameter B in fun !> newBuilder(): B! Please specify it explicitly.
When the code is converted from Java automatically on paste, it sets some stub newBuilder<B>(), where B is not defined and I have no idea what it should be.
We can also discuss here the pattern used in log4j and idiomatic replacement in Kotlin. Here is the related question which seems to be describing recursive generic pattern approach with builder. One more link.
OK, I dug deeper into Kotlin generics, this particular version of Builder pattern and log4j code and this is what I ended up with:
fun main(args: Array<String>) {
val builder = createBuilder(ConsoleAppender::class.java) as ConsoleAppender.Builder<*>?
val appender = builder?.withName("ConsoleAppender")?.build()
println(appender?.name)
}
@Throws(InvocationTargetException::class, IllegalAccessException::class)
private fun createBuilder(clazz: Class<*>): Builder<*>? {
for (method in clazz.declaredMethods) {
if (method.isAnnotationPresent(PluginBuilderFactory::class.java) &&
Modifier.isStatic(method.modifiers) &&
TypeUtil.isAssignable(Builder::class.java, method.returnType)) {
ReflectionUtil.makeAccessible(method)
return method.invoke(null) as Builder<*>
}
}
Function createMethod is adapted from org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.createBuilder(Class<?>), the @Throws annotation can be skipped.
Honestly, reflection is how newBuilder() methods are used internally by log4j itself, which suggest this is how the are supposed to be used. I’m more suprised they work without reflection in regular Java.
BTW, the code can be simplified if you only need it to work with one particular type of Builder:
fun main(args: Array<String>) {
val builder = createBuilder(ConsoleAppender::class.java)
val appender = builder?.withName("ConsoleAppender")?.build()
println(appender?.name)
}
@Throws(InvocationTargetException::class, IllegalAccessException::class)
private fun createBuilder(clazz: Class<*>): ConsoleAppender.Builder<*>? {
for (method in clazz.declaredMethods) {
if (method.name == "newBuilder") {
return method.invoke(null) as ConsoleAppender.Builder<*>
}
}
return null
}
or even
fun main(args: Array<String>) {
val builder = ConsoleAppender::class.java.getMethod("newBuilder").invoke(null) as ConsoleAppender.Builder<*>
val appender = builder.withName("ConsoleAppender").build()
println(appender.name)
}
Notice no null checks in the last version, sice we know that the method “newBuilder” exists in this particular class.
Ok, this works. Now the deeper question. This solution with recursive generics, according to this article, solves builder inheritance problem. What is idiomatic equivalent in Kotlin for that pattern?
Unfortunately, it seems that builder methods have to be chained in order and I cannot figure out why. Maybe someone here will figure out a solution. I’ll keep working on it in the meantime.
Update: @vagran
I solved the chaining order issue, but it seem that every open, non abstract class requires two builder classes: one open and generic, that can be extended in a subclass, and one without a parameter that can actually be instantiated and used for building.