Why kotlin doesn't allow covariant mutablemap to be a delegate? - delegates

I'm new to Kotlin.
When I learn Storing Properties in a Map. I try following usage.
class User(val map: MutableMap<String, String>) {
val name: String by map
}
class User(val map: MutableMap<String, in String>) {
val name: String by map
}
class User(val map: MutableMap<String, out String>) {
val name: String by map
}
The first two are both work, the last one failed.
With out modifier, the bytecode of getName like this:
public final java.lang.String getName();
0 aload_0 [this]
1 getfield kotl.User.name$delegate : java.util.Map [11]
4 astore_1
5 aload_0 [this]
6 astore_2
7 getstatic kotl.User.$$delegatedProperties : kotlin.reflect.KProperty[] [15]
10 iconst_0
11 aaload
12 astore_3
13 aload_1
14 aload_3
15 invokeinterface kotlin.reflect.KProperty.getName() : java.lang.String [19] [nargs: 1]
20 invokestatic kotlin.collections.MapsKt.getOrImplicitDefaultNullable(java.util.Map, java.lang.Object) : java.lang.Object [25]
23 checkcast java.lang.Object [4]
26 aconst_null
27 athrow
Local variable table:
[pc: 0, pc: 28] local: this index: 0 type: kotl.User
As we can see, it will cause a NullPointerException.
Why contravariant is not allowed on a map delegate?
And why kotlin doesn't give me a compile error?

Short answer: it's not a bug in the compiler, but rather an unfortunate consequence of how the signature of operator getValue() is declared for MutableMap.
Long answer:
delegating properties to maps is possible because of the following three operator functions in the standard library:
// for delegating val to read-only map
operator fun <V, V1: V> Map<in String, #Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1
// for delegating var to mutable map
operator fun <V> MutableMap<in String, in V>.getValue(thisRef: Any?, property: KProperty<*>): V
operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V)
Here the use-site variance of the MutableMap receiver is chosen so that one could delegate a property of some type to a map which can store its supertype:
class Sample(val map: MutableMap<String, Any>) {
var stringValue: String by map
var intValue: Int by map
}
Unfortunately, when you try to use an out-projected MutableMap<String, out String> as a delegate for a val property and consequently as a receiver of getValue operator, here what happens:
MutableMap<in String, in V>.getValue overload is choosen, because it has more specific receiver type.
Since the receiver map has out String type argument projection it's unknown what its actual type argument is (it can be a MutableMap<..., String> or a MutableMap<..., SubTypeOfString>), so the only safe option is to assume it is Nothing, which is a subtype of all possible types.
The return type of this function is declared as V which has been inferred to Nothing, and the compiler inserts a check that the actual returned value is of type Nothing, which should always fail, as there couldn't be a value of type Nothing. This check looks like throw null in the bytecode.
I've opened an issue KT-18789 to see what we can be done with the signature of this operator function.
UPD: The signature was fixed in Kotlin 1.2.20
Meanwhile as a workaround you can cast the MutableMap to Map, so that the first overload of getValue is chosen:
class User(val map: MutableMap<String, out String>) {
val name: String by map as Map<String, String>
}

Yeah... the compiler is definitely wrong here. (testing with Kotlin version 1.1.2-5)
First of all, in the case of property delegation to a map, you use the name of the property to look up a value for it in the map.
Using a MutableMap<String, in String>, is equivalent to Java's Map<String, ? super String> which uses contravariance.
Using a MutableMap<String, out String>, is equivalent to Java's Map<String, ? extends String> which uses covariance.
(you mixed the two up)
A covariant type can be used as a producer. A contravariant type can be used as a consumer. (See PECS. sorry, I don't have a Kotlin specific link, but the principle still applies).
Delegation by map uses the second generic type of map as a producer (you get things out of the map), so it should not be possible to use a MutableMap<String, in String> since it's second parameter is a consumer (to put things into).
For some reason, the compiler generates the code it needs for a MutableMap<String, out String> in the case of a MutableMap<String, in String> and this is wrong, as you can see in this example:
class User(val map: MutableMap<String, in String>) {
val name: String by map
}
fun main(args:Array<String>){
val m: MutableMap<String, CharSequence> = mutableMapOf("name" to StringBuilder())
val a = User(m)
val s: String = a.name
}
You will get a class cast exception, because the VM is trying to treat a StringBuilder as a String. But you don't use any explicit casts, so it should be safe.
Unfortunately, it generates garbage (throw null) in the valid use case of out.
In the case of String it doesn't really make sense to use covariance (out), since String is final, but in the case of a different type hierarchy, the only work around I can think of is to manually patch the bytecode, which is a nightmare.
I don't know if there is an existing bug report. I guess we'll just have to wait until this gets fixed.

Related

Typing does not work when creating a map in Kotlin through spring boot #RequestParam

I am using spring boot and have made the following controller where I explicitly specify key and value types.
#PostMapping("DAC/set")
fun setDac(#RequestParam map: HashMap<Int, Float>): ResponseEntity<JSONObject> {
println(map)
return dac()
}
When I send a request like this:
http://localhost:9090/adam6024/DAC/set?a=abc,b=d,d=v
I am getting this output in console:
{a=abc,b=d,d=v}
Why am I not getting an error? Moreover, I cannot add string values to the map, my IDE does not allow me.
I'm not 100% sure, but my guess would be, that this is because of generic type erasure.
Basically, the types defined as part of the generic do not exist any longer during runtime. When Spring reads the request params and puts them into the HashMap, the types are no longer known, thus no error is thrown. During runtime the type of the generic is basically HashMap<Object, Object> (Java) / HashMap<Any?, Any?> (Kotlin).
According to the documentation, #RequestParam may return either Map<String, String> or MultiValueMap<String, String>, when no parameter name is provided.
Thus, you most likely should declare your #RequestParam as Map<String, String> and do any required type conversions yourself.
#PostMapping("DAC/set")
fun setDac(#RequestParam map: Map<String, String>): ResponseEntity<JSONObject> {
val typedMap = map.entries
.associate { (key, value) -> key.toInt() to value.toFloat() }
return dac()
}
If you know the parameter name upfront, you may inject them separately instead, in which case Spring does the type conversion for you.
#PostMapping("DAC/set")
fun setDac(#RequestParam(name = "1") first: Float, #RequestParam(name = "2") second: Float): ResponseEntity<JSONObject> {
// ...
return dac()
}

Specify query hints for SimpleJpaRepository find methods

Goal
I'm implementing a custom base repository with the goal of being able to specify attribute nodes of entity graphs by method argument instead of method annotation. So instead of having annotated methods like
#EntityGraph(attributePaths = ["enrollments"])
fun findByIdFetchingEnrollments(id: Long): Optional<Student>
in my repositories, I just have a base repository with a method like
fun findById(id: ID, vararg attributeNodes: String): Optional<T>
which gives a lot of flexibility because with only one method I can, for example, call
studentRepository.findById(1L, "enrollments")
// or
studentRepository.findById(1L, "favouriteSubjects", "favouriteTeachers")
// and others...
where "enrollments", "favouriteSubjects", and "favouriteTeachers" are lazy-fetch entity fields by default but sometimes are required to be fetched eagerly (to avoid LazyInitializationException)
What I have
Those are my classes:
// Base repository interface. All my repositories will extend this interface
#NoRepositoryBean
interface Repository<T, ID> : JpaRepository<T, ID> {
fun findById(id: ID, vararg attributeNodes: String): Optional<T>
// other find methods
}
// Custom base class. Inherits SimpleJpaRepository
class RepositoryImpl<T, ID>(
val entityInformation: JpaEntityInformation<T, Any?>,
val em: EntityManager
) : SimpleJpaRepository<T, ID>(entityInformation, em), Repository<T,ID> {
// This is basically a copy-paste of SimpleJpaRepository findById method implementation...
override fun findById(id: ID, vararg attributeNodes: String): Optional<T> {
Assert.notNull(id, "The given id must not be null!")
/*
Because 'repositoryMethodMetadata!!.queryHints' is read-only, I have to create a new Map,
put all the queryHints entries in it and put the 'javax.persistence.loadgraph' hint.
*/
val graph = em.createEntityGraph(entityInformation.javaType)
graph.addAttributeNodes(*attributeNodes)
val hints: MutableMap<String, Any?> = HashMap()
hints.putAll(repositoryMethodMetadata!!.queryHints)
hints["javax.persistence.loadgraph"] = graph
if (repositoryMethodMetadata == null) {
return Optional.ofNullable(em.find(domainClass, id, hints))
}
val type = repositoryMethodMetadata!!.lockModeType
return Optional.ofNullable(
if (type == null) em.find(domainClass, id, hints)
else em.find(domainClass, id, type, hints)
)
}
/*
In order to implement the other find methods the same kind of copy-paste implementations
needs to be done, which shouldn't be necessary but I'm not seeing how.
*/
}
#Configuration
#EnableJpaRepositories(
"com.domain.project.repository",
repositoryBaseClass = RepositoryImpl::class)
class ApplicationConfiguration
Problem
The problem is that I'm not seeing a simple way of adding the javax.persistence.loadgraph hint to the query hints that SimpleJpaRepository pass to EnitityManager.find.
I think the best way to solve this would be override the SimpleJpaRepository getQueryHints() method which is used by all find methods in SimpleJpaRepository. Then my code in RepositoryImpl would become much simpler (override all find methods, and for each just add the hint and call super method). But I can't override it because QueryHints class is package-private.
If SimpleJpaRepository metadata property were mutable, I could also add to it query hints (which in turn are considered in getQueryHints() method), but all its properties are read-only and I think sometimes metadata is null (not sure on this but it is marked as #Nullable).

Impact of #JsonTypeInfo in REST endpoints when concrete implementations are used

I'm wondering about the effects of adding a #JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#class") to an interface.
My use case is that I have an interface Message with many subtypes. I want to be able to deserialize and serialize lists of messages in one endpoint.
My classes:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#class")
interface Message: Serializable
data class Message1(
val name: String,
val age: Int
)
data class Message2(
val name: String,
val nickName: String
)
And the respective endpoint:
#RestController
class MessageController {
#GetMapping("/messages")
fun getEndpoints(): List<Message> {
return listOf(
Message1("Marco", 22),
Message2("Polo", "Poli")
)
}
}
So far so good - but now I want another endpoint that uses one of the explicit classes and I get a serialization error in my test that #class is missing - I don't want to send that when I'm using a concrete class anyhow.
#RestController
class MessageController {
#PostMapping("/add1")
fun add(#RequestBody content: Message1) {
// do something
}
}
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Missing type id when trying to resolve subtype of [simple type, class com.tractive.tap.message.request.RequestDataDTO]: missing type id property '#class'; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class com.tractive.tap.message.request.RequestDataDTO]: missing type id property '#class'
at [Source: (PushbackInputStream); line: 1, column: 51]
Why is the #class expected even though I'm using a concrete class? Is this expected behavior or am I doing something wrong?
Well, it is expected because by annotating the class with #JsonTypeInfo - via class-interface inheritance - you have explicitly instructed your Jackson to expect this information.
#JsonTypeInfo accepts parameter defaultImpl of type Class<?> that will be used
if type identifier is either not present, or can not be mapped to a registered type
You could use that to default your deserialisation to one type of message, preferably the most widely used explicitly in your api. For other types, you would still have to include the class info for Jackson.

Spring Boot #ConfigurationProperties YAML referencing

I really like the #ConfigurationProperties-functionality of Spring to load my configuration properties via YAML into a Java class.
Normally I would use it in a way, that I have the following yaml-file:
lst:
typ:
A: "FLT"
B: "123"
C: "345"
D: "TTS"
The type-attribute would be mapped to a Java-Map. Now i would like to have a solution to reference yaml-fragments in the yaml-file itself, so that I could reuse the reference to the fragment:
lst: ${typs}
typs:
A: "FLT"
B: "123"
C: "345"
D: "TTS"
Is that possible with Spring and #ConfigurationProperties?
I believe it is only possible to use placeholder with string properties.
This leaves you with 2 options:
repeat the values;
OR define the map as a String of properties (source: https://stackoverflow.com/a/28370899/641627).
Solution - Define the map as a comma-separated String of key-values
The whole explanation is provided if you click on the link above. I'll walk you through it.
(1) application.yml:
prop1: A:FLT, B:123, C...
prop2: ${prop1}
(2) Define a String to Map converter/splitter (from #FedericoPeraltaSchaffner's answer)
#Component("PropertySplitter")
public class PropertySplitter {
public Map<String, String> map(String property) {
return this.map(property, ",");
}
private Map<String, String> map(String property, String splitter) {
return Splitter.on(splitter).omitEmptyStrings().trimResults().withKeyValueSeparator(":").split(property);
}
}
(3) Inject the map using #Value and the splitter:
#Value("#{PropertySplitter.map('${prop1}')}")
Map<String, String> prop1;
#Value("#{PropertySplitter.map('${prop2}')}")
Map<String, String> prop2;

Deserializing classes with lazy properties using Gson and Kotlin 1.0 beta 4

Using Gson, I want to deserialize a Kotlin class that contains a lazy property.
With Kotlin 1.0 beta 4 I get the following error during object deserialization:
Caused by: java.lang.InstantiationException: can't instantiate class kotlin.Lazy
With Kotlin 1.0 beta 2, I used to mark the property with the #Transient annotaiton to tell Gson to skip it. With beta 4 this is not possible anymore, as the annotation causes a compile error.
This annotation is not applicable to target 'member property without backing field'
I can’t figure out how to fix this. Any ideas?
Edit: the lazy property is serialized to JSON ("my_lazy_prop$delegate":{}), but this is not what I want as it is computed from other properties. I suppose if I find a way to prevent the property from being serialized the deserialization crash would be fixed.
Since Kotlin 1.0 simply mark the field like this to ignore it during de/serialization:
#delegate:Transient
val field by lazy { ... }
The reason is that the delegate field is not a backing field actually so it was forbidden. One of the workarounds is to implement ExclusionStrategy: https://stackoverflow.com/a/27986860/1460833
Something like that:
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class GsonTransient
object TransientExclusionStrategy : ExclusionStrategy {
override fun shouldSkipClass(type: Class<*>): Boolean = false
override fun shouldSkipField(f: FieldAttributes): Boolean =
f.getAnnotation(GsonTransient::class.java) != null
|| f.name.endsWith("\$delegate")
}
fun gson() = GsonBuilder()
.setExclusionStrategies(TransientExclusionStrategy)
.create()
See related ticket https://youtrack.jetbrains.com/issue/KT-10502
The other workaround is to serialize lazy values as well:
object SDForLazy : JsonSerializer<Lazy<*>>, JsonDeserializer<Lazy<*>> {
override fun serialize(src: Lazy<*>, typeOfSrc: Type, context: JsonSerializationContext): JsonElement =
context.serialize(src.value)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Lazy<*> =
lazyOf<Any?>(context.deserialize(json, (typeOfT as ParameterizedType).actualTypeArguments[0]))
}
class KotlinNamingPolicy(val delegate: FieldNamingStrategy = FieldNamingPolicy.IDENTITY) : FieldNamingStrategy {
override fun translateName(f: Field): String =
delegate.translateName(f).removeSuffix("\$delegate")
}
Usage example:
data class C(val o: Int) {
val f by lazy { 1 }
}
fun main(args: Array<String>) {
val gson = GsonBuilder()
.registerTypeAdapter(Lazy::class.java, SDForLazy)
.setFieldNamingStrategy(KotlinNamingPolicy())
.create()
val s = gson.toJson(C(0))
println(s)
val c = gson.fromJson(s, C::class.java)
println(c)
println(c.f)
}
that will produce the following output:
{"f":1,"o":0}
C(o=0)
1
As explained by other answers, the delegate field should not be serialized.
You can achieve this with transient in the delegate field, as proposed by #Fabian Zeindl:
#delegate:Transient
val field by lazy { ... }
or skipping all delegate fields in the GsonBuilder, as proposed by #Sergey Mashkov:
GsonBuilder().setExclusionStrategies(object : ExclusionStrategy {
override fun shouldSkipClass(type: Class<*>): Boolean = false
override fun shouldSkipField(f: FieldAttributes): Boolean = f.name.endsWith("\$delegate")
}
However, you may face a NullPointerException if your class doesn't have a no-argument constructor.
It happens because when Gson doesn't find the no-argument constructor, it will use a ObjectConstructor with an UnsafeAllocator using Reflection to construct your object. (see https://stackoverflow.com/a/18645370). This will erase the Kotlin creation of the delegate field.
To fix it, either create a no-argument constructor in your class, or use Gson InstanceCreator to provide Gson with a default object.
GsonBuilder().registerTypeAdapter(YourClass::class, object : InstanceCreator<YourClass> {
override fun createInstance(type: Type?) = YourClass("defaultValue")
})

Resources