-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
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
- Loading branch information
1 parent
98e97a8
commit 954ed1a
Showing
19 changed files
with
616 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package tech.ydb.yoj.databind.schema.reflect; | ||
|
||
import kotlin.jvm.JvmClassMappingKt; | ||
import kotlin.reflect.KCallable; | ||
import kotlin.reflect.jvm.ReflectJvmMapping; | ||
import tech.ydb.yoj.databind.FieldValueType; | ||
import tech.ydb.yoj.databind.schema.Column; | ||
import tech.ydb.yoj.databind.schema.FieldValueException; | ||
|
||
import javax.annotation.Nullable; | ||
import java.lang.reflect.Type; | ||
|
||
/** | ||
* Represents a Kotlin data class component for the purposes of YOJ data-binding. | ||
*/ | ||
public final class KotlinDataClassComponent implements ReflectField { | ||
private final KCallable<?> callable; | ||
|
||
private final String name; | ||
private final Type genericType; | ||
private final Class<?> type; | ||
private final FieldValueType valueType; | ||
private final Column column; | ||
|
||
private final ReflectType<?> reflectType; | ||
|
||
public KotlinDataClassComponent(Reflector reflector, String name, KCallable<?> callable) { | ||
this.callable = callable; | ||
|
||
this.name = name; | ||
|
||
var kReturnType = callable.getReturnType(); | ||
this.genericType = ReflectJvmMapping.getJavaType(kReturnType); | ||
this.type = JvmClassMappingKt.getJavaClass(kReturnType); | ||
this.column = type.getAnnotation(Column.class); | ||
this.valueType = FieldValueType.forJavaType(genericType, column); | ||
this.reflectType = reflector.reflectFieldType(genericType, valueType); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Object getValue(Object containingObject) { | ||
try { | ||
return callable.call(containingObject); | ||
} catch (Exception e) { | ||
throw new FieldValueException(e, getName(), containingObject); | ||
} | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public Type getGenericType() { | ||
return genericType; | ||
} | ||
|
||
@Override | ||
public Class<?> getType() { | ||
return type; | ||
} | ||
|
||
@Override | ||
public FieldValueType getValueType() { | ||
return valueType; | ||
} | ||
|
||
@Override | ||
public Column getColumn() { | ||
return column; | ||
} | ||
|
||
@Override | ||
public ReflectType<?> getReflectType() { | ||
return reflectType; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "KotlinDataClassComponent[val " + name + ": " + genericType.getTypeName() + "]"; | ||
} | ||
} |
95 changes: 95 additions & 0 deletions
95
databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package tech.ydb.yoj.databind.schema.reflect; | ||
|
||
import com.google.common.base.Preconditions; | ||
import kotlin.jvm.JvmClassMappingKt; | ||
import kotlin.reflect.KCallable; | ||
import kotlin.reflect.KMutableProperty; | ||
import kotlin.reflect.KParameter; | ||
import kotlin.reflect.full.KClasses; | ||
import kotlin.reflect.jvm.ReflectJvmMapping; | ||
|
||
import java.lang.reflect.Constructor; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
import static java.util.stream.Collectors.toMap; | ||
|
||
/** | ||
* Represents a Kotlin data class for the purposes of YOJ data-binding. | ||
*/ | ||
public final class KotlinDataClassType<T> implements ReflectType<T> { | ||
private final Class<T> type; | ||
private final Constructor<T> constructor; | ||
private final List<ReflectField> fields; | ||
|
||
public KotlinDataClassType(Reflector reflector, Class<T> type) { | ||
this.type = type; | ||
|
||
var kClass = JvmClassMappingKt.getKotlinClass(type); | ||
var kClassName = kClass.getQualifiedName(); | ||
Preconditions.checkArgument(kClass.isData(), | ||
"'%s' is not a data class", | ||
kClassName); | ||
|
||
var primaryKtConstructor = KClasses.getPrimaryConstructor(kClass); | ||
Preconditions.checkArgument(primaryKtConstructor != null, | ||
"'%s' has no primary constructor", | ||
kClassName); | ||
|
||
var primaryJavaConstructor = ReflectJvmMapping.getJavaConstructor(primaryKtConstructor); | ||
Preconditions.checkArgument(primaryJavaConstructor != null, | ||
"Could not get Java Constructor<%s> from KFunction: %s", | ||
kClassName, primaryKtConstructor); | ||
this.constructor = primaryJavaConstructor; | ||
this.constructor.setAccessible(true); | ||
|
||
var functions = KClasses.getDeclaredMemberFunctions(kClass).stream() | ||
.filter(c -> KotlinDataClassTypeFactory.isComponentMethodName(c.getName()) | ||
&& c.getParameters().size() == 1 | ||
&& Objects.equals(kClass, c.getParameters().get(0).getType().getClassifier())) | ||
.collect(toMap(KCallable::getName, m -> m)); | ||
|
||
var mutableProperties = KClasses.getDeclaredMemberProperties(kClass).stream() | ||
.filter(p -> p instanceof KMutableProperty) | ||
.collect(toMap(KCallable::getName, m -> m)); | ||
|
||
List<ReflectField> fields = new ArrayList<>(); | ||
int n = 1; | ||
for (KParameter param : primaryKtConstructor.getParameters()) { | ||
var paramName = param.getName(); | ||
|
||
Preconditions.checkArgument(!mutableProperties.containsKey(paramName), | ||
"Mutable constructor arguments are not allowed in '%s', but got: '%s'", | ||
kClassName, paramName); | ||
|
||
var callable = functions.get("component" + n); | ||
Preconditions.checkState(callable != null, | ||
"Could not find component%s() in '%s'", | ||
n, kClassName); | ||
fields.add(new KotlinDataClassComponent(reflector, paramName, callable)); | ||
n++; | ||
} | ||
this.fields = List.copyOf(fields); | ||
} | ||
|
||
@Override | ||
public Constructor<T> getConstructor() { | ||
return constructor; | ||
} | ||
|
||
@Override | ||
public List<ReflectField> getFields() { | ||
return fields; | ||
} | ||
|
||
@Override | ||
public Class<T> getRawType() { | ||
return type; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "KotlinDataClassType[" + type + "]"; | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassTypeFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package tech.ydb.yoj.databind.schema.reflect; | ||
|
||
import kotlin.Metadata; | ||
import tech.ydb.yoj.databind.FieldValueType; | ||
import tech.ydb.yoj.databind.schema.reflect.StdReflector.TypeFactory; | ||
|
||
import java.lang.annotation.Annotation; | ||
import java.lang.reflect.Method; | ||
|
||
import static java.util.Arrays.stream; | ||
|
||
public final class KotlinDataClassTypeFactory implements TypeFactory { | ||
public static final TypeFactory instance = new KotlinDataClassTypeFactory(); | ||
|
||
private static final int KIND_CLASS = 1; | ||
|
||
private KotlinDataClassTypeFactory() { | ||
} | ||
|
||
@Override | ||
public int priority() { | ||
return 300; | ||
} | ||
|
||
@Override | ||
public boolean matches(Class<?> rawType, FieldValueType fvt) { | ||
return KotlinReflectionDetector.kotlinAvailable | ||
&& stream(rawType.getDeclaredAnnotations()).anyMatch(this::isKotlinClassMetadata) | ||
&& stream(rawType.getDeclaredMethods()).anyMatch(this::isComponentGetter); | ||
} | ||
|
||
private boolean isKotlinClassMetadata(Annotation ann) { | ||
return Metadata.class.equals(ann.annotationType()) && ((Metadata) ann).k() == KIND_CLASS; | ||
} | ||
|
||
private boolean isComponentGetter(Method m) { | ||
return m.getParameterCount() == 0 && isComponentMethodName(m.getName()); | ||
} | ||
|
||
@Override | ||
public <R> ReflectType<R> create(Reflector reflector, Class<R> rawType, FieldValueType fvt) { | ||
return new KotlinDataClassType<>(reflector, rawType); | ||
} | ||
|
||
static boolean isComponentMethodName(String name) { | ||
if (!name.startsWith("component")) { | ||
return false; | ||
} | ||
|
||
try { | ||
int n = Integer.parseInt(name.substring("component".length()), 10); | ||
return n >= 1; | ||
} catch (NumberFormatException e) { | ||
return false; | ||
} | ||
} | ||
} |
49 changes: 49 additions & 0 deletions
49
databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinReflectionDetector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package tech.ydb.yoj.databind.schema.reflect; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
final class KotlinReflectionDetector { | ||
private static final Logger log = LoggerFactory.getLogger(KotlinReflectionDetector.class); | ||
|
||
private KotlinReflectionDetector() { | ||
} | ||
|
||
public static final boolean kotlinAvailable = detectKotlinReflection(); | ||
|
||
private static boolean detectKotlinReflection() { | ||
var cl = classLoader(); | ||
|
||
try { | ||
Class.forName("kotlin.Metadata", false, cl); | ||
} catch (ClassNotFoundException e) { | ||
return false; | ||
} | ||
|
||
try { | ||
Class.forName("kotlin.reflect.full.KClasses", false, cl); | ||
return true; | ||
} catch (ClassNotFoundException e) { | ||
log.warn("YOJ has detected Kotlin but not kotlin-reflect. Kotlin data classes won't work as Entities.", e); | ||
return false; | ||
} | ||
} | ||
|
||
private static ClassLoader classLoader() { | ||
ClassLoader cl = null; | ||
try { | ||
cl = Thread.currentThread().getContextClassLoader(); | ||
} catch (Exception ignore) { | ||
} | ||
if (cl == null) { | ||
cl = KotlinDataClassType.class.getClassLoader(); | ||
if (cl == null) { | ||
try { | ||
cl = ClassLoader.getSystemClassLoader(); | ||
} catch (Exception ignore) { | ||
} | ||
} | ||
} | ||
return cl; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.