Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for java getter #518

Merged
merged 8 commits into from
Nov 10, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package com.linecorp.kotlinjdsl.querymodel.jpql.path.impl
import com.linecorp.kotlinjdsl.Internal
import com.linecorp.kotlinjdsl.querymodel.jpql.entity.Entity
import com.linecorp.kotlinjdsl.querymodel.jpql.path.Path
import kotlin.reflect.KProperty1
import kotlin.reflect.KCallable

@Internal
data class JpqlEntityProperty<T : Any, V> internal constructor(
val entity: Entity<*>,
val property: KProperty1<in T, V>,
val entity: Entity<T>,
val property: KCallable<V>,
) : Path<V & Any>
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.linecorp.kotlinjdsl.querymodel.jpql.path.impl

import com.linecorp.kotlinjdsl.Internal
import com.linecorp.kotlinjdsl.querymodel.jpql.path.Path
import kotlin.reflect.KProperty1
import kotlin.reflect.KCallable

@Internal
data class JpqlPathProperty<T : Any, V> internal constructor(
val path: Path<T>,
val property: KProperty1<in T, V>,
val property: KCallable<V>,
) : Path<V & Any>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.linecorp.kotlinjdsl.render.jpql.introspector

import com.linecorp.kotlinjdsl.SinceJdsl
import kotlin.reflect.KCallable
import kotlin.reflect.KClass

/**
Expand All @@ -20,4 +21,14 @@ class CombinedJpqlIntrospector(
override fun introspect(type: KClass<*>): JpqlEntityDescription? {
return primary.introspect(type) ?: secondary.introspect(type)
}

/**
* Get the entity information by introspecting KCallable.
*
* If the primary introspector introspects this KCallable, it returns the result of the primary introspector.
* Otherwise, it returns the result of the secondary introspector.
*/
override fun introspect(property: KCallable<*>): JpqlPropertyDescription? {
return primary.introspect(property) ?: secondary.introspect(property)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.linecorp.kotlinjdsl.render.jpql.introspector

import com.linecorp.kotlinjdsl.SinceJdsl
import kotlin.reflect.KCallable
import kotlin.reflect.KClass

/**
Expand All @@ -14,4 +15,11 @@ interface JpqlIntrospector {
*/
@SinceJdsl("3.0.0")
fun introspect(type: KClass<*>): JpqlEntityDescription?

/**
* Introspects the KCallable to get the entity information.
* If it cannot introspect this KCallable, it returns null.
*/
@SinceJdsl("3.1.0")
fun introspect(property: KCallable<*>): JpqlPropertyDescription?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.linecorp.kotlinjdsl.render.jpql.introspector

import com.linecorp.kotlinjdsl.SinceJdsl

/**
* Interface to represent the property information.
*/
@SinceJdsl("3.1.0")
interface JpqlPropertyDescription {
@SinceJdsl("3.1.0")
val name: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.linecorp.kotlinjdsl.SinceJdsl
import com.linecorp.kotlinjdsl.render.AbstractRenderContextElement
import com.linecorp.kotlinjdsl.render.RenderContext
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KCallable
import kotlin.reflect.KClass

/**
Expand All @@ -15,7 +16,8 @@ class JpqlRenderIntrospector(
) : AbstractRenderContextElement(Key) {
companion object Key : RenderContext.Key<JpqlRenderIntrospector>

private val tableLookupCache: MutableMap<KClass<*>, JpqlEntityDescription> = ConcurrentHashMap()
private val classTableLookupCache: MutableMap<KClass<*>, JpqlEntityDescription> = ConcurrentHashMap()
private val propertyTableLookupCache: MutableMap<KCallable<*>, JpqlPropertyDescription> = ConcurrentHashMap()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table prefix is a misnomer on my part - I named it table because I was planning to have native queries at that time, but now that it's JPQL-only, I think it's okay to name it entityLookupCache and propertyLookupCache.


/**
* Creates a new introspector by combining this introspector and the introspector.
Expand All @@ -38,8 +40,22 @@ class JpqlRenderIntrospector(
return getCachedDescription(clazz)
}

/**
* Introspects the KCallable to get the property information.
*/
@SinceJdsl("3.1.0")
fun introspect(property: KCallable<*>): JpqlPropertyDescription {
return getCachedDescription(property)
}

private fun getCachedDescription(clazz: KClass<*>): JpqlEntityDescription {
return tableLookupCache.computeIfAbsent(clazz) {
return classTableLookupCache.computeIfAbsent(clazz) {
getDescription(it)
}
}

private fun getCachedDescription(property: KCallable<*>): JpqlPropertyDescription {
return propertyTableLookupCache.computeIfAbsent(property) {
getDescription(it)
}
}
Expand All @@ -48,4 +64,9 @@ class JpqlRenderIntrospector(
return introspector.introspect(clazz)
?: throw IllegalStateException("There is no description for ${clazz.java.name}")
}

private fun getDescription(property: KCallable<*>): JpqlPropertyDescription {
return introspector.introspect(property)
?: throw IllegalStateException("There is no description for ${property.name}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package com.linecorp.kotlinjdsl.render.jpql.introspector.impl
import com.linecorp.kotlinjdsl.Internal
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlEntityDescription
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlIntrospector
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlPropertyDescription
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KFunction1
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotations

/**
* Introspector that introspects KClass using [jakarta.persistence.Entity].
* Introspector that introspects KClass and KCallable using [jakarta.persistence.Entity].
*/
@Internal
class JakartaJpqlIntrospector : JpqlIntrospector {
Expand All @@ -20,6 +24,21 @@ class JakartaJpqlIntrospector : JpqlIntrospector {
null
}
}

override fun introspect(property: KCallable<*>): JpqlPropertyDescription? {
return when (property) {
is KProperty1<*, *> -> JpqlProperty(property.name)
is KFunction1<*, *> -> JpqlProperty(resolvePropertyName(property))
else -> null
}
}

private fun resolvePropertyName(getter: KFunction1<*, *>): String =
if (getter.name.startsWith("is")) {
getter.name
} else {
getter.name.removePrefix("get").replaceFirstChar { it.lowercase() }
}
}

private data class JakartaEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package com.linecorp.kotlinjdsl.render.jpql.introspector.impl
import com.linecorp.kotlinjdsl.Internal
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlEntityDescription
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlIntrospector
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlPropertyDescription
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KFunction1
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotations

/**
* Introspector that introspects KClass using [javax.persistence.Entity].
* Introspector that introspects KClass and KCallable using [javax.persistence.Entity].
*/
@Internal
class JavaxJpqlIntrospector : JpqlIntrospector {
Expand All @@ -20,6 +24,21 @@ class JavaxJpqlIntrospector : JpqlIntrospector {
null
}
}

override fun introspect(property: KCallable<*>): JpqlPropertyDescription? {
return when (property) {
is KProperty1<*, *> -> JpqlProperty(property.name)
is KFunction1<*, *> -> JpqlProperty(resolvePropertyName(property))
else -> null
}
}

private fun resolvePropertyName(getter: KFunction1<*, *>): String =
if (getter.name.startsWith("is")) {
getter.name
} else {
getter.name.removePrefix("get").replaceFirstChar { it.lowercase() }
}
Copy link
Member

@shouwn shouwn Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the Property Introspector can be separated from Javax and Jakarta.

JpqlIntrospector is designed to delegate to another Intropsector if it cannot introspect itself. Therefore, I think we can reduce the duplication by creating a separate class that only introspects properties and adding it to the Context together.

class JavaxJpqlEntityIntrospector : JpqlIntrospector {
    override fun introspect(type: KClass<*>): JpqlEntityDescription? {
        // ...
    }

    override fun introspect(property: KCallable<*>): JpqlPropertyDescription? {
        return null
    }
}

class JakartaJpqlEntityIntrospector : JpqlIntrospector {
    override fun introspect(type: KClass<*>): JpqlEntityDescription? {
        // ...
    }

    override fun introspect(property: KCallable<*>): JpqlPropertyDescription? {
        return null
    }
}

class KotlinStyleJpqlPropertyIntrospector : JpqlIntrospector {
    override fun introspect(type: KClass<*>): JpqlEntityDescription? {
        return null
    }

    override fun introspect(property: KCallable<*>): JpqlPropertyDescription? {
        return when (property) {
            is KProperty1<*, *> -> JpqlProperty(property.name)
            is KFunction1<*, *> -> JpqlProperty(resolvePropertyName(property))
            else -> null
        }
    }
    
    // ...
}

private class DefaultModule : JpqlRenderModule {
    override fun setupModule(context: JpqlRenderModule.SetupContext) {
        // ...

        context.appendIntrospector(KotlinStyleJpqlPropertyIntrospector())

        // ...
    }
}

I think also you could create two abstract classes, JpqlEntityIntrospector and JpqlPropertyIntrospector, that implement JpqlIntropsector.

}

private data class JavaxEntity(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.linecorp.kotlinjdsl.render.jpql.introspector.impl

import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlPropertyDescription

internal data class JpqlProperty(
override val name: String,
) : JpqlPropertyDescription
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.linecorp.kotlinjdsl.render.jpql.serializer.impl
import com.linecorp.kotlinjdsl.Internal
import com.linecorp.kotlinjdsl.querymodel.jpql.path.impl.JpqlEntityProperty
import com.linecorp.kotlinjdsl.render.RenderContext
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlRenderIntrospector
import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer
import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter
import kotlin.reflect.KClass
Expand All @@ -14,8 +15,11 @@ class JpqlEntityPropertySerializer : JpqlSerializer<JpqlEntityProperty<*, *>> {
}

override fun serialize(part: JpqlEntityProperty<*, *>, writer: JpqlWriter, context: RenderContext) {
val introspector = context.getValue(JpqlRenderIntrospector)
val property = introspector.introspect(part.property)

writer.write(part.entity.alias)
writer.write(".")
writer.write(part.property.name)
writer.write(property.name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.linecorp.kotlinjdsl.render.jpql.serializer.impl
import com.linecorp.kotlinjdsl.Internal
import com.linecorp.kotlinjdsl.querymodel.jpql.path.impl.JpqlPathProperty
import com.linecorp.kotlinjdsl.render.RenderContext
import com.linecorp.kotlinjdsl.render.jpql.introspector.JpqlRenderIntrospector
import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer
import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer
import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter
Expand All @@ -16,9 +17,11 @@ class JpqlPathPropertySerializer : JpqlSerializer<JpqlPathProperty<*, *>> {

override fun serialize(part: JpqlPathProperty<*, *>, writer: JpqlWriter, context: RenderContext) {
val delegate = context.getValue(JpqlRenderSerializer)
val introspector = context.getValue(JpqlRenderIntrospector)
val property = introspector.introspect(part.property)

delegate.serialize(part.path, writer, context)
writer.write(".")
writer.write(part.property.name)
writer.write(property.name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.assertj.core.api.WithAssertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.reflect.KCallable
import kotlin.reflect.KClass

@ExtendWith(MockKExtension::class)
class CombinedJpqlIntrospectorTest : WithAssertions {
Expand All @@ -24,6 +26,10 @@ class CombinedJpqlIntrospectorTest : WithAssertions {
override val name: String = "entityName1"
}

private val propertyDescription1 = object : JpqlPropertyDescription {
override val name: String = "propertyName1"
}

@BeforeEach
fun setUp() {
sut = CombinedJpqlIntrospector(
Expand All @@ -33,9 +39,9 @@ class CombinedJpqlIntrospectorTest : WithAssertions {
}

@Test
fun `introspect() return the description of the primary, when the primary returns non null`() {
fun `introspect(type) return the description of the primary, when the primary returns non null`() {
// given
every { introspector1.introspect(any()) } returns entityDescription1
every { introspector1.introspect(any<KClass<*>>()) } returns entityDescription1

// when
val actual = sut.introspect(Book::class)
Expand All @@ -49,10 +55,10 @@ class CombinedJpqlIntrospectorTest : WithAssertions {
}

@Test
fun `introspect() return the description of the secondary, when the primary returns null`() {
fun `introspect(type) return the description of the secondary, when the primary returns null`() {
// given
every { introspector1.introspect(any()) } returns null
every { introspector2.introspect(any()) } returns entityDescription1
every { introspector1.introspect(any<KClass<*>>()) } returns null
every { introspector2.introspect(any<KClass<*>>()) } returns entityDescription1

// when
val actual = sut.introspect(Book::class)
Expand All @@ -65,4 +71,38 @@ class CombinedJpqlIntrospectorTest : WithAssertions {
introspector2.introspect(Book::class)
}
}

@Test
fun `introspect(property) return the description of the primary, when the primary returns non null`() {
// given
every { introspector1.introspect(any<KCallable<*>>()) } returns propertyDescription1

// when
val actual = sut.introspect(Book::title)

// then
assertThat(actual).isEqualTo(propertyDescription1)

verifySequence {
introspector1.introspect(Book::title)
}
}

@Test
fun `introspect(property) return the description of the secondary, when the primary returns null`() {
// given
every { introspector1.introspect(any<KCallable<*>>()) } returns null
every { introspector2.introspect(any<KCallable<*>>()) } returns propertyDescription1

// when
val actual = sut.introspect(Book::title)

// then
assertThat(actual).isEqualTo(propertyDescription1)

verifySequence {
introspector1.introspect(Book::title)
introspector2.introspect(Book::title)
}
}
}
Loading