Skip to content

Instantly share code, notes, and snippets.

@jdmwood
Created November 1, 2019 10:09
Show Gist options
  • Save jdmwood/9d97b4a2252e3b2596c67a87d56e7d19 to your computer and use it in GitHub Desktop.
Save jdmwood/9d97b4a2252e3b2596c67a87d56e7d19 to your computer and use it in GitHub Desktop.
package com.yourpackage
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import javax.persistence.*
import kotlin.jvm.internal.Reflection
import kotlin.reflect.KCallable
import org.hibernate.annotations.Subselect
import org.junit.Assert.assertFalse
import org.junit.Test
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
import org.springframework.core.type.filter.AnnotationTypeFilter
const val PACKAGE = "com.yourpackage"
class CheckKotlinJPAClasses {
@Test
@Throws(ClassNotFoundException::class, NoSuchFieldException::class)
fun testAllKotlinJPAFields() {
val sb = StringBuilder()
// Find all Entity classes
val scanner = ClassPathScanningCandidateComponentProvider(false)
scanner.addIncludeFilter(AnnotationTypeFilter(Entity::class.java))
val fail = scanner.findCandidateComponents(PACKAGE)
.map { Class.forName(it.beanClassName) }
.filter { it.getDeclaredAnnotation(Metadata::class.java) != null } // Ignore non kotlin classes
.map { checkForNullableMismatch(sb, it) }
.any { it }
assertFalse(sb.toString(), fail)
}
/**
* Check that the Kotlin nullness matches the JPA nullness of fields
*/
private fun checkForNullableMismatch(sb: StringBuilder, clazz: Class<*>): Boolean {
// Ignore views (with @Subselect)
if (clazz.getDeclaredAnnotation(Subselect::class.java) != null) {
return false
}
var fail: Boolean = false
val kotlinClass = Reflection.createKotlinClass(clazz)
val members = kotlinClass.members as Collection<KCallable<*>>
for (member in members) {
val expectedNullable = member.returnType.isMarkedNullable
// For some reason the KClass getAnnotations() doesn't work so use the Java one
var declaredField: Field? = null
try {
declaredField = clazz.getDeclaredField(member.name)
} catch (e: NoSuchFieldException) {
// It's not a Kotlin property
continue
}
// Ignore special cases where we have transient loaded fields
if (Modifier.isTransient(declaredField!!.modifiers) || declaredField.getDeclaredAnnotation(Transient::class.java) != null) {
continue
}
// Ignore embedded
if (declaredField.getDeclaredAnnotation(Embedded::class.java) != null) {
continue
}
val declaredAnnotations = declaredField.declaredAnnotations
var nullables = 0
var nonNullables = 0
for (annotation in declaredAnnotations) {
if (annotation is Basic) {
if (annotation.optional) {
nullables++
} else {
nonNullables++
}
}
if (annotation is Column) {
if (annotation.nullable) {
nullables++
} else {
nonNullables++
}
}
if (annotation is ManyToOne) {
if (annotation.optional) {
nullables++
} else {
nonNullables++
}
}
if (annotation is OneToOne) {
if (annotation.optional) {
nullables++
} else {
nonNullables++
}
}
if (annotation is Id) {
nonNullables++
}
}
if (declaredAnnotations.isEmpty()) {
nullables++
}
if (nullables > 0 && nonNullables > 0) {
sb.append(String.format("Found conflicting nullables on @Column/@Basic for %s\n", member))
fail = true
}
val wasNullable = nonNullables == 0
if (expectedNullable != wasNullable) {
sb.append(
String.format(
"Found JPA field [%s] with nullable mismatch. The field was nullable=%b but the JPA annotations were nullable=%b. Check your @Basic/@Column annotations.\n",
member,
expectedNullable,
wasNullable
)
)
fail = true
}
}
return fail
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment