Skip to content

Commit 954ed1a

Browse files
authored
#7: Support Kotlin data classes in StdReflector (#8)
Kotlin support is enabled by default if you have `kotlin-stdlib` *AND* `kotlin-reflection` 1.9.x on your classpath. Note that you can use Kotlin data classes exposed as Java records (`@JvmRecord`) even if you have *NO* Kotlin dependencies on your classpath. Fixes: #7
1 parent 98e97a8 commit 954ed1a

File tree

19 files changed

+616
-15
lines changed

19 files changed

+616
-15
lines changed

bom/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>tech.ydb.yoj</groupId>
88
<artifactId>yoj-bom</artifactId>
9-
<version>1.0.3-SNAPSHOT</version>
9+
<version>1.1.0-SNAPSHOT</version>
1010
<packaging>pom</packaging>
1111

1212
<name>YOJ - Bill of Materials</name>

databind/pom.xml

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<parent>
1111
<groupId>tech.ydb.yoj</groupId>
1212
<artifactId>yoj-parent</artifactId>
13-
<version>1.0.3-SNAPSHOT</version>
13+
<version>1.1.0-SNAPSHOT</version>
1414
<relativePath>../pom.xml</relativePath>
1515
</parent>
1616

@@ -34,5 +34,59 @@
3434
<groupId>javax.annotation</groupId>
3535
<artifactId>javax.annotation-api</artifactId>
3636
</dependency>
37+
<dependency>
38+
<groupId>org.slf4j</groupId>
39+
<artifactId>slf4j-api</artifactId>
40+
</dependency>
41+
42+
<!-- Optional (Kotlin support) -->
43+
<dependency>
44+
<groupId>org.jetbrains.kotlin</groupId>
45+
<artifactId>kotlin-reflect</artifactId>
46+
<optional>true</optional>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.jetbrains.kotlin</groupId>
50+
<artifactId>kotlin-stdlib</artifactId>
51+
<optional>true</optional>
52+
</dependency>
53+
54+
<dependency>
55+
<groupId>org.apache.logging.log4j</groupId>
56+
<artifactId>log4j-slf4j-impl</artifactId>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.apache.logging.log4j</groupId>
61+
<artifactId>log4j-core</artifactId>
62+
<scope>test</scope>
63+
</dependency>
3764
</dependencies>
65+
66+
<build>
67+
<plugins>
68+
<plugin>
69+
<groupId>org.jetbrains.kotlin</groupId>
70+
<artifactId>kotlin-maven-plugin</artifactId>
71+
<executions>
72+
<execution>
73+
<id>compileTests</id>
74+
<phase>process-test-sources</phase>
75+
<goals>
76+
<goal>test-compile</goal>
77+
</goals>
78+
<configuration>
79+
<sourceDirs>
80+
<source>src/test/kotlin</source>
81+
<source>target/generated-sources/annotations</source>
82+
</sourceDirs>
83+
<jvmTarget>17</jvmTarget>
84+
</configuration>
85+
</execution>
86+
</executions>
87+
</plugin>
88+
</plugins>
89+
</build>
90+
91+
3892
</project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package tech.ydb.yoj.databind.schema.reflect;
2+
3+
import kotlin.jvm.JvmClassMappingKt;
4+
import kotlin.reflect.KCallable;
5+
import kotlin.reflect.jvm.ReflectJvmMapping;
6+
import tech.ydb.yoj.databind.FieldValueType;
7+
import tech.ydb.yoj.databind.schema.Column;
8+
import tech.ydb.yoj.databind.schema.FieldValueException;
9+
10+
import javax.annotation.Nullable;
11+
import java.lang.reflect.Type;
12+
13+
/**
14+
* Represents a Kotlin data class component for the purposes of YOJ data-binding.
15+
*/
16+
public final class KotlinDataClassComponent implements ReflectField {
17+
private final KCallable<?> callable;
18+
19+
private final String name;
20+
private final Type genericType;
21+
private final Class<?> type;
22+
private final FieldValueType valueType;
23+
private final Column column;
24+
25+
private final ReflectType<?> reflectType;
26+
27+
public KotlinDataClassComponent(Reflector reflector, String name, KCallable<?> callable) {
28+
this.callable = callable;
29+
30+
this.name = name;
31+
32+
var kReturnType = callable.getReturnType();
33+
this.genericType = ReflectJvmMapping.getJavaType(kReturnType);
34+
this.type = JvmClassMappingKt.getJavaClass(kReturnType);
35+
this.column = type.getAnnotation(Column.class);
36+
this.valueType = FieldValueType.forJavaType(genericType, column);
37+
this.reflectType = reflector.reflectFieldType(genericType, valueType);
38+
}
39+
40+
@Nullable
41+
@Override
42+
public Object getValue(Object containingObject) {
43+
try {
44+
return callable.call(containingObject);
45+
} catch (Exception e) {
46+
throw new FieldValueException(e, getName(), containingObject);
47+
}
48+
}
49+
50+
@Override
51+
public String getName() {
52+
return name;
53+
}
54+
55+
@Override
56+
public Type getGenericType() {
57+
return genericType;
58+
}
59+
60+
@Override
61+
public Class<?> getType() {
62+
return type;
63+
}
64+
65+
@Override
66+
public FieldValueType getValueType() {
67+
return valueType;
68+
}
69+
70+
@Override
71+
public Column getColumn() {
72+
return column;
73+
}
74+
75+
@Override
76+
public ReflectType<?> getReflectType() {
77+
return reflectType;
78+
}
79+
80+
@Override
81+
public String toString() {
82+
return "KotlinDataClassComponent[val " + name + ": " + genericType.getTypeName() + "]";
83+
}
84+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package tech.ydb.yoj.databind.schema.reflect;
2+
3+
import com.google.common.base.Preconditions;
4+
import kotlin.jvm.JvmClassMappingKt;
5+
import kotlin.reflect.KCallable;
6+
import kotlin.reflect.KMutableProperty;
7+
import kotlin.reflect.KParameter;
8+
import kotlin.reflect.full.KClasses;
9+
import kotlin.reflect.jvm.ReflectJvmMapping;
10+
11+
import java.lang.reflect.Constructor;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.Objects;
15+
16+
import static java.util.stream.Collectors.toMap;
17+
18+
/**
19+
* Represents a Kotlin data class for the purposes of YOJ data-binding.
20+
*/
21+
public final class KotlinDataClassType<T> implements ReflectType<T> {
22+
private final Class<T> type;
23+
private final Constructor<T> constructor;
24+
private final List<ReflectField> fields;
25+
26+
public KotlinDataClassType(Reflector reflector, Class<T> type) {
27+
this.type = type;
28+
29+
var kClass = JvmClassMappingKt.getKotlinClass(type);
30+
var kClassName = kClass.getQualifiedName();
31+
Preconditions.checkArgument(kClass.isData(),
32+
"'%s' is not a data class",
33+
kClassName);
34+
35+
var primaryKtConstructor = KClasses.getPrimaryConstructor(kClass);
36+
Preconditions.checkArgument(primaryKtConstructor != null,
37+
"'%s' has no primary constructor",
38+
kClassName);
39+
40+
var primaryJavaConstructor = ReflectJvmMapping.getJavaConstructor(primaryKtConstructor);
41+
Preconditions.checkArgument(primaryJavaConstructor != null,
42+
"Could not get Java Constructor<%s> from KFunction: %s",
43+
kClassName, primaryKtConstructor);
44+
this.constructor = primaryJavaConstructor;
45+
this.constructor.setAccessible(true);
46+
47+
var functions = KClasses.getDeclaredMemberFunctions(kClass).stream()
48+
.filter(c -> KotlinDataClassTypeFactory.isComponentMethodName(c.getName())
49+
&& c.getParameters().size() == 1
50+
&& Objects.equals(kClass, c.getParameters().get(0).getType().getClassifier()))
51+
.collect(toMap(KCallable::getName, m -> m));
52+
53+
var mutableProperties = KClasses.getDeclaredMemberProperties(kClass).stream()
54+
.filter(p -> p instanceof KMutableProperty)
55+
.collect(toMap(KCallable::getName, m -> m));
56+
57+
List<ReflectField> fields = new ArrayList<>();
58+
int n = 1;
59+
for (KParameter param : primaryKtConstructor.getParameters()) {
60+
var paramName = param.getName();
61+
62+
Preconditions.checkArgument(!mutableProperties.containsKey(paramName),
63+
"Mutable constructor arguments are not allowed in '%s', but got: '%s'",
64+
kClassName, paramName);
65+
66+
var callable = functions.get("component" + n);
67+
Preconditions.checkState(callable != null,
68+
"Could not find component%s() in '%s'",
69+
n, kClassName);
70+
fields.add(new KotlinDataClassComponent(reflector, paramName, callable));
71+
n++;
72+
}
73+
this.fields = List.copyOf(fields);
74+
}
75+
76+
@Override
77+
public Constructor<T> getConstructor() {
78+
return constructor;
79+
}
80+
81+
@Override
82+
public List<ReflectField> getFields() {
83+
return fields;
84+
}
85+
86+
@Override
87+
public Class<T> getRawType() {
88+
return type;
89+
}
90+
91+
@Override
92+
public String toString() {
93+
return "KotlinDataClassType[" + type + "]";
94+
}
95+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package tech.ydb.yoj.databind.schema.reflect;
2+
3+
import kotlin.Metadata;
4+
import tech.ydb.yoj.databind.FieldValueType;
5+
import tech.ydb.yoj.databind.schema.reflect.StdReflector.TypeFactory;
6+
7+
import java.lang.annotation.Annotation;
8+
import java.lang.reflect.Method;
9+
10+
import static java.util.Arrays.stream;
11+
12+
public final class KotlinDataClassTypeFactory implements TypeFactory {
13+
public static final TypeFactory instance = new KotlinDataClassTypeFactory();
14+
15+
private static final int KIND_CLASS = 1;
16+
17+
private KotlinDataClassTypeFactory() {
18+
}
19+
20+
@Override
21+
public int priority() {
22+
return 300;
23+
}
24+
25+
@Override
26+
public boolean matches(Class<?> rawType, FieldValueType fvt) {
27+
return KotlinReflectionDetector.kotlinAvailable
28+
&& stream(rawType.getDeclaredAnnotations()).anyMatch(this::isKotlinClassMetadata)
29+
&& stream(rawType.getDeclaredMethods()).anyMatch(this::isComponentGetter);
30+
}
31+
32+
private boolean isKotlinClassMetadata(Annotation ann) {
33+
return Metadata.class.equals(ann.annotationType()) && ((Metadata) ann).k() == KIND_CLASS;
34+
}
35+
36+
private boolean isComponentGetter(Method m) {
37+
return m.getParameterCount() == 0 && isComponentMethodName(m.getName());
38+
}
39+
40+
@Override
41+
public <R> ReflectType<R> create(Reflector reflector, Class<R> rawType, FieldValueType fvt) {
42+
return new KotlinDataClassType<>(reflector, rawType);
43+
}
44+
45+
static boolean isComponentMethodName(String name) {
46+
if (!name.startsWith("component")) {
47+
return false;
48+
}
49+
50+
try {
51+
int n = Integer.parseInt(name.substring("component".length()), 10);
52+
return n >= 1;
53+
} catch (NumberFormatException e) {
54+
return false;
55+
}
56+
}
57+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tech.ydb.yoj.databind.schema.reflect;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
final class KotlinReflectionDetector {
7+
private static final Logger log = LoggerFactory.getLogger(KotlinReflectionDetector.class);
8+
9+
private KotlinReflectionDetector() {
10+
}
11+
12+
public static final boolean kotlinAvailable = detectKotlinReflection();
13+
14+
private static boolean detectKotlinReflection() {
15+
var cl = classLoader();
16+
17+
try {
18+
Class.forName("kotlin.Metadata", false, cl);
19+
} catch (ClassNotFoundException e) {
20+
return false;
21+
}
22+
23+
try {
24+
Class.forName("kotlin.reflect.full.KClasses", false, cl);
25+
return true;
26+
} catch (ClassNotFoundException e) {
27+
log.warn("YOJ has detected Kotlin but not kotlin-reflect. Kotlin data classes won't work as Entities.", e);
28+
return false;
29+
}
30+
}
31+
32+
private static ClassLoader classLoader() {
33+
ClassLoader cl = null;
34+
try {
35+
cl = Thread.currentThread().getContextClassLoader();
36+
} catch (Exception ignore) {
37+
}
38+
if (cl == null) {
39+
cl = KotlinDataClassType.class.getClassLoader();
40+
if (cl == null) {
41+
try {
42+
cl = ClassLoader.getSystemClassLoader();
43+
} catch (Exception ignore) {
44+
}
45+
}
46+
}
47+
return cl;
48+
}
49+
}

databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/PojoType.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public PojoType(@NonNull Reflector reflector, @NonNull Class<T> type) {
6363
.toList();
6464
} else {
6565
this.fields = Stream.of(type.getDeclaredFields())
66-
.filter(tech.ydb.yoj.databind.schema.reflect.PojoType::isEntityField)
66+
.filter(PojoType::isEntityField)
6767
.<ReflectField>map(f -> new PojoField(reflector, f))
6868
.toList();
6969
}
@@ -94,7 +94,7 @@ private static boolean isEntityField(Field f) {
9494
// FIXME: this is NOT the best way to find all-args ctor
9595
private static <T> Constructor<T> findAllArgsCtor(Class<T> type) {
9696
long instanceFieldCount = Stream.of(type.getDeclaredFields())
97-
.filter(tech.ydb.yoj.databind.schema.reflect.PojoType::isEntityField)
97+
.filter(PojoType::isEntityField)
9898
.count();
9999

100100
@SuppressWarnings("unchecked") Constructor<T> ctor = (Constructor<T>) Stream.of(type.getDeclaredConstructors())

0 commit comments

Comments
 (0)