Lately I was trying to read a config file with Kotlin. The config file is written in YAML and I’m using SnakeYAML for reading the file.
Consider the following file:
# Size of the universe
universeSize:
maxGalaxies: 1
maxSystems: 3
maxPlanets: 3
# Starter planet settings
starterPlanet:
# Starting resources
resources:
crystal: 200
gas: 100
energy: 800
# Round time in seconds
roundTime: 5
How to model the class hierarchy so that my Kotlin program can read the files? First I tried Kotlin data classes:
data class UniverseSizeDto(var maxGalaxies: Int, var maxSystems: Int, var maxPlanets: Int)
data class ResourcesDto(var crystal: Int, var gas: Int, var energy: Int)
data class StarterPlanetDto(var resources: ResourcesDto)
data class ConfigDto(var universeSize: UniverseSizeDto, var starterPlanet: StarterPlanetDto, var roundTime: Int)
Reading the config file is relatively easy:
fun loadFromFile(path: Path): ConfigDto {
val yaml = Yaml(Constructor(ConfigDto::class.java))
return Files.newBufferedReader(path).use {
yaml.load(it) as ConfigDto
}
}
Unfortunately this results in the following exception:
Exception in thread "main" Can't construct a java object for tag:yaml.org,2002:restwars.business.config.Config$ConfigDto; exception=java.lang.NoSuchMethodException: restwars.business.config.Config$ConfigDto.<init>()
in 'reader', line 2, column 1:
universeSize:
^
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:349)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:141)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:127)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:450)
at org.yaml.snakeyaml.Yaml.load(Yaml.java:393)
Damn. Kotlin data classes have no parameterless constructor by default. They get a parameterless constructor if all parameters have a default value. Naah, I don’t want this. Okay, so no data classes then. Next try:
class UniverseSizeDto {
var maxGalaxies: Int
var maxSystems: Int
var maxPlanets: Int
}
class ResourcesDto {
var crystal: Int
var gas: Int
var energy: Int
}
class StarterPlanetDto {
var resources: ResourcesDto
}
class ConfigDto {
var universeSize: UniverseSizeDto
var starterPlanet: StarterPlanetDto
var roundTime: Int
}
Argh, doesn’t compile:
Error:(15, 9) Kotlin: Property must be initialized or be abstract
Error:(16, 9) Kotlin: Property must be initialized or be abstract
Error:(17, 9) Kotlin: Property must be initialized or be abstract
Error:(25, 9) Kotlin: Property must be initialized or be abstract
Error:(26, 9) Kotlin: Property must be initialized or be abstract
Error:(27, 9) Kotlin: Property must be initialized or be abstract
Error:(35, 9) Kotlin: Property must be initialized or be abstract
Error:(43, 9) Kotlin: Property must be initialized or be abstract
Error:(44, 9) Kotlin: Property must be initialized or be abstract
Error:(45, 9) Kotlin: Property must be initialized or be abstract
Okay, Kotlin forces us to initialize the variable either in the initializer or in the constructor. After snooping around in the documentation, i found late-initialized properties:
Normally, properties declared as having a non-null type must be initialized in the constructor. However, fairly often this is not convenient. For example, properties can be initialized through dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null initializer in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class.
Okay, so some other guy at JetBrains had a similar problem. Let’s give it a try:
class UniverseSizeDto {
lateinit var maxGalaxies: Int
lateinit var maxSystems: Int
lateinit var maxPlanets: Int
}
class ResourcesDto {
lateinit var crystal: Int
lateinit var gas: Int
lateinit var energy: Int
}
class StarterPlanetDto {
lateinit var resources: ResourcesDto
}
class ConfigDto {
lateinit var universeSize: UniverseSizeDto
lateinit var starterPlanet: StarterPlanetDto
lateinit var roundTime: Int
}
Argh again, doesn’t compile:
Error:(15, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Error:(16, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Error:(17, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Error:(25, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Error:(26, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Error:(27, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Error:(45, 9) Kotlin: 'lateinit' modifier is not allowed on primitive type properties
Now the compiler is whining about the lateinit
bevor the Int
fields. Dammit. You can write it this way:
class UniverseSizeDto {
lateinit var maxGalaxies: Integer
lateinit var maxSystems: Integer
lateinit var maxPlanets: Integer
}
class ResourcesDto {
lateinit var crystal: Integer
lateinit var gas: Integer
lateinit var energy: Integer
}
class StarterPlanetDto {
lateinit var resources: ResourcesDto
}
class ConfigDto {
lateinit var universeSize: UniverseSizeDto
lateinit var starterPlanet: StarterPlanetDto
lateinit var roundTime: Integer
}
Now IntelliJ is shouting at me because I should use Int
instead of Integer
. At least it compiles and throws no exceptions. But i don’t like warnings either.
Looks like I’m stuck with default values, or I could write my own SnakeYAML constructor. Sigh, default values then.
Now the code looks like this:
data class UniverseSizeDto(var maxGalaxies: Int = 0, var maxSystems: Int = 0, var maxPlanets: Int = 0)
data class ResourcesDto(var crystal: Int = 0, var gas: Int = 0, var energy: Int = 0)
data class StarterPlanetDto(var resources: ResourcesDto = ResourcesDto())
data class ConfigDto(var universeSize: UniverseSizeDto = UniverseSizeDto(), var starterPlanet: StarterPlanetDto = StarterPlanetDto(), var roundTime: Int = 0)
If someone knows a solution to my dilemma, please feel free to contact me.
Update 2016-02-27
I found a pretty solution using Jackson.