Skip to content
This repository was archived by the owner on Jan 20, 2023. It is now read-only.
Merged
6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
plugins {
id("maven")
id("java")
id("org.jetbrains.kotlin.jvm") version "1.3.71"
id("org.jetbrains.kotlin.jvm") version "1.3.72"
id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
}

group = "com.mapk"
version = "0.22"
version = "0.23"

java {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand All @@ -18,7 +18,7 @@ buildscript {
}

dependencies {
classpath(kotlin("gradle-plugin", version = "1.3.71"))
classpath(kotlin("gradle-plugin"))
}
}

Expand All @@ -30,10 +30,10 @@ repositories {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(kotlin("reflect"))
api("com.github.ProjectMapK:Shared:0.10")
api("com.github.ProjectMapK:Shared:0.11")

// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.6.1") {
testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.6.2") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
// 現状プロパティ名の変換はテストでしか使っていないのでtestImplementation
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlin.reflect.jvm.jvmName
class BoundKMapper<S : Any, D : Any> private constructor(
private val function: KFunctionForCall<D>,
src: KClass<S>,
parameterNameConverter: (String) -> String = { it }
parameterNameConverter: (String) -> String
) {
constructor(function: KFunction<D>, src: KClass<S>, parameterNameConverter: (String) -> String = { it }) : this(
KFunctionForCall(function), src, parameterNameConverter
Expand All @@ -45,7 +45,7 @@ class BoundKMapper<S : Any, D : Any> private constructor(
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
.mapNotNull {
val temp = srcPropertiesMap[parameterNameConverter(it.getAliasOrName()!!)]?.let { property ->
BoundParameterForMap(it, property)
BoundParameterForMap.newInstance(it, property)
}

// 必須引数に対応するプロパティがsrcに定義されていない場合エラー
Expand Down
86 changes: 64 additions & 22 deletions src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,83 @@ package com.mapk.kmapper

import com.mapk.core.EnumMapper
import java.lang.IllegalArgumentException
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.javaGetter

@Suppress("UNCHECKED_CAST")
internal class BoundParameterForMap<S : Any>(val param: KParameter, property: KProperty1<S, *>) {
val map: (S) -> Any?
internal sealed class BoundParameterForMap<S> {
abstract val param: KParameter
protected abstract val propertyGetter: Method

init {
// ゲッターが無いならエラー
val propertyGetter = property.javaGetter
?: throw IllegalArgumentException("${property.name} does not have getter.")
propertyGetter.isAccessible = true
abstract fun map(src: S): Any?

val paramClazz = param.type.classifier as KClass<*>
val propertyClazz = property.returnType.classifier as KClass<*>
private class Plain<S : Any>(
override val param: KParameter,
override val propertyGetter: Method
) : BoundParameterForMap<S>() {
override fun map(src: S): Any? = propertyGetter.invoke(src)
}

private class UseConverter<S : Any>(
override val param: KParameter,
override val propertyGetter: Method,
private val converter: KFunction<*>
) : BoundParameterForMap<S>() {
override fun map(src: S): Any? = converter.call(propertyGetter.invoke(src))
}

private class ToEnum<S : Any>(
override val param: KParameter,
override val propertyGetter: Method,
private val paramClazz: Class<*>
) : BoundParameterForMap<S>() {
override fun map(src: S): Any? = EnumMapper.getEnum(paramClazz, propertyGetter.invoke(src) as String)
}

private class ToString<S : Any>(
override val param: KParameter,
override val propertyGetter: Method
) : BoundParameterForMap<S>() {
override fun map(src: S): String? = propertyGetter.invoke(src).toString()
}

companion object {
fun <S : Any> newInstance(param: KParameter, property: KProperty1<S, *>): BoundParameterForMap<S> {
// ゲッターが無いならエラー
val propertyGetter = property.javaGetter
?: throw IllegalArgumentException("${property.name} does not have getter.")
propertyGetter.isAccessible = true

val paramClazz = param.type.classifier as KClass<*>
val propertyClazz = property.returnType.classifier as KClass<*>

val converter = paramClazz.getConverters()
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
.let {
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")
// コンバータが取れた場合
paramClazz.getConverters()
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
.let {
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")

it.singleOrNull()?.second
it.singleOrNull()?.second
}?.let {
return UseConverter(param, propertyGetter, it)
}

if (paramClazz.isSubclassOf(propertyClazz)) {
return Plain(param, propertyGetter)
}

map = when {
converter != null -> { { converter.call(propertyGetter.invoke(it)) } }
paramClazz.isSubclassOf(propertyClazz) -> { { propertyGetter.invoke(it) } }
paramClazz.java.isEnum && propertyClazz == String::class -> { {
EnumMapper.getEnum(paramClazz.java, propertyGetter.invoke(it) as String)
} }
paramClazz == String::class -> { { propertyGetter.invoke(it).toString() } }
else -> throw IllegalArgumentException("Can not convert $propertyClazz to $paramClazz")
val javaClazz = paramClazz.java

return when {
javaClazz.isEnum && propertyClazz == String::class -> ToEnum(param, propertyGetter, javaClazz)
paramClazz == String::class -> ToString(param, propertyGetter)
else -> throw IllegalArgumentException("Can not convert $propertyClazz to $paramClazz")
}
}
}
}
31 changes: 18 additions & 13 deletions src/main/kotlin/com/mapk/kmapper/KMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ class KMapper<T : Any> private constructor(
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
.associate { (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it) }

private val getCache: ConcurrentMap<KClass<*>, List<(Any, ArgumentBucket) -> Unit>> = ConcurrentHashMap()
private val getCache: ConcurrentMap<KClass<*>, List<ArgumentBinder>> = ConcurrentHashMap()

private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) {
val clazz = src::class

// キャッシュヒットしたら登録した内容に沿って取得処理を行う
getCache[clazz]?.let { getters ->
// 取得対象フィールドは十分絞り込んでいると考えられるため、終了判定は行わない
getters.forEach { it(src, argumentBucket) }
getters.forEach { it.bindArgument(src, argumentBucket) }
return
}

val tempCacheArrayList = ArrayList<(Any, ArgumentBucket) -> Unit>()
val tempBinderArrayList = ArrayList<ArgumentBinder>()

src::class.memberProperties.forEach outer@{ property ->
// propertyが公開されていない場合は処理を行わない
Expand All @@ -64,19 +64,14 @@ class KMapper<T : Any> private constructor(
parameterMap[alias ?: property.name]?.let { param ->
javaGetter.isAccessible = true

val tempCache = { value: Any, bucket: ArgumentBucket ->
// 初期化済みであれば高コストな取得処理は行わない
if (!bucket.containsKey(param.param)) {
// javaGetterを呼び出す方が高速
bucket.putIfAbsent(param.param, javaGetter.invoke(value)?.let { param.mapObject(it) })
}
}
tempCache(src, argumentBucket)
tempCacheArrayList.add(tempCache)
val binder = ArgumentBinder(param, javaGetter)

binder.bindArgument(src, argumentBucket)
tempBinderArrayList.add(binder)
// キャッシュの整合性を保つため、ここでは終了判定を行わない
}
}
getCache.putIfAbsent(clazz, tempCacheArrayList)
getCache.putIfAbsent(clazz, tempBinderArrayList)
}

private fun bindArguments(argumentBucket: ArgumentBucket, src: Map<*, *>) {
Expand Down Expand Up @@ -131,3 +126,13 @@ class KMapper<T : Any> private constructor(
return function.call(bucket)
}
}

private class ArgumentBinder(private val param: ParameterForMap<*>, private val javaGetter: Method) {
fun bindArgument(value: Any, bucket: ArgumentBucket) {
// 初期化済みであれば高コストな取得処理は行わない
if (!bucket.containsKey(param.param)) {
// javaGetterを呼び出す方が高速
bucket.putIfAbsent(param.param, javaGetter.invoke(value)?.let { param.mapObject(it) })
}
}
}
38 changes: 29 additions & 9 deletions src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,33 @@ internal class ParameterForMap<T : Any> private constructor(val param: KParamete
// リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい
private val converters: Set<Pair<KClass<*>, KFunction<T>>> = clazz.getConverters()

private val convertCache: ConcurrentMap<KClass<*>, (Any) -> Any?> = ConcurrentHashMap()
private val convertCache: ConcurrentMap<KClass<*>, ParameterProcessor> = ConcurrentHashMap()

fun <U : Any> mapObject(value: U): Any? {
val valueClazz: KClass<*> = value::class

// 取得方法のキャッシュが有ればそれを用いる
convertCache[valueClazz]?.let { return it(value) }
convertCache[valueClazz]?.let { return it.process(value) }

// パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる
if (clazz.isSuperclassOf(valueClazz)) {
convertCache.putIfAbsent(valueClazz) { it }
convertCache.putIfAbsent(valueClazz, ParameterProcessor.Plain)
return value
}

val converter: KFunction<*>? = converters.getConverter(valueClazz)

val lambda: (Any) -> Any? = when {
val processor: ParameterProcessor = when {
// converterに一致する組み合わせが有れば設定されていればそれを使う
converter != null -> { { converter.call(it) } }
converter != null -> ParameterProcessor.UseConverter(converter)
// 要求された値がenumかつ元が文字列ならenum mapperでマップ
javaClazz.isEnum && value is String -> { { EnumMapper.getEnum(javaClazz, it as String) } }
javaClazz.isEnum && value is String -> ParameterProcessor.ToEnum(javaClazz)
// 要求されているパラメータがStringならtoStringする
clazz == String::class -> { { it.toString() } }
clazz == String::class -> ParameterProcessor.ToString
else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz")
}
convertCache.putIfAbsent(valueClazz, lambda)
return lambda(value)
convertCache.putIfAbsent(valueClazz, processor)
return processor.process(value)
}

companion object {
Expand All @@ -50,3 +50,23 @@ internal class ParameterForMap<T : Any> private constructor(val param: KParamete
}
}
}

private sealed class ParameterProcessor {
abstract fun process(value: Any): Any?

object Plain : ParameterProcessor() {
override fun process(value: Any): Any? = value
}

class UseConverter(private val converter: KFunction<*>) : ParameterProcessor() {
override fun process(value: Any): Any? = converter.call(value)
}

class ToEnum(private val javaClazz: Class<*>) : ParameterProcessor() {
override fun process(value: Any): Any? = EnumMapper.getEnum(javaClazz, value as String)
}

object ToString : ParameterProcessor() {
override fun process(value: Any): Any? = value.toString()
}
}